androidmedia-playerscreeninstantiationexception

How to prevent mediaplayer to stop when screen goes off?


I have a mediaplayer in a Music class that is called from another secondary Activity. It works fine.

But when screen goes off (either by timeout or button), the music stops playing, and when coming back and try to close the activity the program goes to "App Not Responding", because an IllegalStateException at a query like mediaplayer.isPlaying().

How can I prevent the mediaplayer to stop when screen goes off?

Does it have to be through a service??

Assuming that the answer is yes, I tried to convert the Music class into a service (see below). I also added <service android:enabled="true" android:name=".Music" /> into the Manifest.xml, and I am calling the Music class like this:

startService(new Intent(getBaseContext(), Music.class));
Music track = Music(fileDescriptor);

The only 2 new lines in the main Activity are startService(new Intent(getBaseContext(), Music.class)); and stopService(new Intent(getBaseContext(), Music.class));, together with the corresponding imports.

But now I get InstantiationException error because can't instantiate class when trying to start the service. What am I missing?

This is the exception:

E/AndroidRuntime(16642): FATAL EXCEPTION: main
E/AndroidRuntime(16642): java.lang.RuntimeException: Unable to instantiate service com.floritfoto.apps.ave.Music:                                                                             java.lang.InstantiationException: can't instantiate class com.floritfoto.apps.ave.Music; no empty constructor                                                                   
E/AndroidRuntime(16642):    at android.app.ActivityThread.handleCreateService(ActivityThread.java:2249)
E/AndroidRuntime(16642):    at android.app.ActivityThread.access$1600(ActivityThread.java:127)
E/AndroidRuntime(16642):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1213)
E/AndroidRuntime(16642):    at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(16642):    at android.os.Looper.loop(Looper.java:137)
E/AndroidRuntime(16642):    at android.app.ActivityThread.main(ActivityThread.java:4507)
E/AndroidRuntime(16642):    at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(16642):    at java.lang.reflect.Method.invoke(Method.java:511)
E/AndroidRuntime(16642):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980)
E/AndroidRuntime(16642):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747)
E/AndroidRuntime(16642):    at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime(16642): Caused by: java.lang.InstantiationException: can't instantiate class com.floritfoto.apps.ave.Music; no empty constructor
E/AndroidRuntime(16642):    at java.lang.Class.newInstanceImpl(Native Method)
E/AndroidRuntime(16642):    at java.lang.Class.newInstance(Class.java:1319)
E/AndroidRuntime(16642):    at android.app.ActivityThread.handleCreateService(ActivityThread.java:2246)
E/AndroidRuntime(16642):    ... 10 more

and this is the Music.class:

package com.floritfoto.apps.ave;

import java.io.FileDescriptor;
import java.io.IOException;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.IBinder;
import android.widget.Toast;

public class Music extends Service implements OnCompletionListener{
    MediaPlayer mediaPlayer;
    boolean isPrepared = false;

    //// TEstes de servico
    @Override
    public void onCreate() {
        super.onCreate();
        info("Servico criado!");
    }
    @Override
    public void onDestroy() {
        info("Servico fudeu!");
    }
    @Override
    public void onStart(Intent intent, int startid) {
        info("Servico started!");
    }   
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    public void info(String txt) {
        Toast toast = Toast.makeText(getApplicationContext(), txt, Toast.LENGTH_LONG);
        toast.show();
    }
    //// Fim testes de servico

    public Music(FileDescriptor fileDescriptor){
        mediaPlayer = new MediaPlayer();
        try{
            mediaPlayer.setDataSource(fileDescriptor);
            mediaPlayer.prepare();
            isPrepared = true;
            mediaPlayer.setOnCompletionListener(this);
        } catch(Exception ex){
            throw new RuntimeException("Couldn't load music, uh oh!");
        }
    }

    public void onCompletion(MediaPlayer mediaPlayer) {
        synchronized(this){
            isPrepared = false;
        }
    }

    public void play() {
        if(mediaPlayer.isPlaying()) return;
        try{
            synchronized(this){
                if(!isPrepared){
                    mediaPlayer.prepare();
                }
                mediaPlayer.seekTo(0);
                mediaPlayer.start();
            }
        } catch(IllegalStateException ex){
            ex.printStackTrace();
        } catch(IOException ex){
            ex.printStackTrace();
        }
    }

    public void stop() {
        mediaPlayer.stop();
        synchronized(this){
            isPrepared = false;
        }
    }

    public void switchTracks(){
        mediaPlayer.seekTo(0);
        mediaPlayer.pause();
    }

    public void pause() {
        mediaPlayer.pause();
    }

    public boolean isPlaying() {
        return mediaPlayer.isPlaying();
    }

    public boolean isLooping() {
        return mediaPlayer.isLooping();
    }

    public void setLooping(boolean isLooping) {
        mediaPlayer.setLooping(isLooping);
    }

    public void setVolume(float volumeLeft, float volumeRight) {
        mediaPlayer.setVolume(volumeLeft, volumeRight);
    }

    public String getDuration() {
        return String.valueOf((int)(mediaPlayer.getDuration()/1000));
    }
    public void dispose() {
        if(mediaPlayer.isPlaying()){
            stop();
        }
        mediaPlayer.release();
    }
}

Solution

  • This line from Logcat is the important one:

    Caused by: java.lang.InstantiationException: can't instantiate class com.floritfoto.apps.ave.Music; no empty constructor
    

    Your service needs another constructor that takes no arguments:

    public Music() {
        super("Music");
    }
    

    EDIT:

    Using a service is the correct approach if you want to keep the music playing when the screen is off. However, the phone will try to sleep when the screen is off, and this can interrupt your MediaPlayer.

    The most reliable solution is to use a partial WakeLock to prevent the device from sleeping while you're playing music. Be sure to release the WakeLock properly when you're not actively playing music; otherwise the battery will drain.

    You may also want to use startForeground(), which will reduce the risk of your service being killed when there is memory pressure. It will also create a nice user experience by showing a persistent notification when your service is running.

    Instantiating the Music class with Music track = Music(fileDescriptor); is probably doing some harm. A better approach is to pass the file descriptor as an Extra in the Intent that you pass to startService():

    Intent serviceIntent = new Intent(this, Music.class);
    serviceIntent.putExtra("ServiceFileDescriptor", fileDescriptor);
    startService(serviceIntent);
    

    Then, retrieve the file descriptor from that same Intent when it's passed to your service's onStartCommand() method:

    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStart();
    
        Bundle bundle = intent.getExtras();
        // NOTE: The next line will vary depending on the data type for the file
        // descriptor. I'm assuming that it's an int.
        int fileDescriptor = bundle.getIntExtra("ServiceFileDescriptor");
        mediaPlayer = new MediaPlayer();
        try {
            mediaPlayer.setDataSource(fileDescriptor);
            ...
        ...
        return START_STICKY;
    }
    

    A few of things to note here. I've moved the code from your original constructor (which should be removed) into onStartCommand(). You can remove the onStart() method as well, since it will only be called on pre-2.0 devices. If you want to support modern Android versions, you'll need to use onStartCommand() instead. Finally, the START_STICKY return value will ensure that the service stays running until you call stopService() from your activity.

    EDIT 2:

    Using a service enables your users to move between activities without interrupting the MediaPlayer. You don't have much control over how long an Activity will stay in memory, but an active Service (especially if you call startForeground()) won't be killed unless there is very strong memory pressure.

    To interact with the MediaPlayer after the service is started, you have a couple of options. You can pass additional commands to the service by creating Intents and using the action string (and/or some extras) to tell the service what you would like it to do. Just call startActivity() again with the new Intent, and onStartCommand() will be called in the service, at which point you can manipulate the MediaPlayer. The second option is to use a bound service (example here) and to bind/unbind each time you enter/leave an activity that needs to communicate with the service. Using a bound service "feels" as though you're directly manipulating the service, but it's also more complex since you need to manage binding and unbinding.