0

I'm trying to build a music app in Android, although I feel ridiculously newbie sometimes. I have researched a lot, tried my best at getting the hang of how MediaPlayer, MediaBrowserCompat / MediaBrowserServiceCompat, MediaController and Services work, tens of unfortunately old tutorials about how to build one.

My biggest issue was that most of them tended to use the IBinder functionality and intents to bind and start the musicPlaybackService, while google's documentation used these MediaBrowser and MediaBrowserService APIs, both approaches new and honestly quite hard and overwhelming for me.

I've learned a lot so far, but it was hard. The two slightly better tutorials I've found are https://www.sitepoint.com/a-step-by-step-guide-to-building-an-android-audio-player-app/, and https://code.tutsplus.com/tutorials/background-audio-in-android-with-mediasessioncompat--cms-27030 and they use the first and the second approach. My version of the app is what I managed to make by putting together all the pieces I've learned.

A lot of bugs and issues I managed to figure out on my own, but I've encountered a NullPointer exception that I simply don't know how to solve. The debugger is weird as well on this one, it feels almost like every time the error comes from another place of the program; sometimes it stops at the breakpoints I put, and then running the debugger again with the exact same code and breakpoints, it skips them and goes straight into the Runtime error.

This is my Manifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ecebuc.gesmediaplayer">

<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <service
        android:name=".MediaPlaybackService"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MEDIA_BUTTON" />
            <action android:name="android.media.AUDIO_BECOMING_NOISY" />
            <action android:name="android.media.browse.MediaBrowserService" />
        </intent-filter>
    </service>

    <receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
        <intent-filter>
            <action android:name="android.intent.action.MEDIA_BUTTON" />
            <action android:name="android.media.AUDIO_BECOMING_NOISY" />
        </intent-filter>
    </receiver>
</application>

Then this is my MainActivity.java

        public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private final int REQUEST_CODE_GESPLAYER_EXTERNAL_STORAGE = 101;
    private static final int STATE_PAUSED = 0;
    private static final int STATE_PLAYING = 1;
    private static int currentState;

    private MediaBrowserCompat gesMediaBrowser;
    private MediaControllerCompat gesMediaController;
    public MediaControllerCompat.TransportControls gesPlaybackController;

    private Button playPauseToggleButton;

    private MediaBrowserCompat.ConnectionCallback mediaBrowserCallbacks = new MediaBrowserCompat.ConnectionCallback() {
        @Override
        public void onConnected() {
            super.onConnected();
            try {
                //create the media controller and register the callbacks to stay in sync
                gesMediaController = new MediaControllerCompat(MainActivity.this, gesMediaBrowser.getSessionToken());
                gesMediaController.registerCallback(mediaControllerCallbacks);

                //save the controller and define the easy access transport controls in the object
                MediaControllerCompat.setMediaController(MainActivity.this, gesMediaController);
                gesPlaybackController = gesMediaController.getTransportControls();

                //Display initial state
                MediaMetadataCompat metadata = gesMediaController.getMetadata();
                PlaybackStateCompat pbState = gesMediaController.getPlaybackState();

            } catch( RemoteException e ) {

            }
        }
        @Override
        public void onConnectionSuspended() {
            // The Service has crashed. Disable transport controls until it automatically reconnects
        }
        @Override
        public void onConnectionFailed() {
            // The Service has refused our connection
            Log.d("onConnectionFail: ", "the service hasn't been able to connect");
        }
    };
    private MediaControllerCompat.Callback mediaControllerCallbacks = new MediaControllerCompat.Callback() {
        @Override
        public void onMetadataChanged(MediaMetadataCompat metadata) {
            super.onMetadataChanged(metadata);
        }
        @Override
        public void onPlaybackStateChanged(PlaybackStateCompat state) {
            super.onPlaybackStateChanged(state);
            if( state == null ) {
                Log.d("onPlaybackChange: ", "the state is null");
                Toast.makeText(MainActivity.this,
                        "onPlaybackStateChange: the state is null",
                        Toast.LENGTH_SHORT)
                        .show();
                return;
            }

            switch( state.getState() ) {
                case PlaybackStateCompat.STATE_PLAYING: {
                    currentState = STATE_PLAYING;
                    break;
                }
                case PlaybackStateCompat.STATE_PAUSED: {
                    currentState = STATE_PAUSED;
                    break;
                }
            }
        }
        @Override
        public void onSessionDestroyed(){
            // Override to handle the session being destroyed
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //grab the buttons for media playback control
        playPauseToggleButton = (Button)findViewById(R.id.playPause_btn);

        //request permissions for external storage
        if (ContextCompat.checkSelfPermission(this,
                android.Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            // Permission have not been granted
            ActivityCompat.requestPermissions(this,
                    new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
                    REQUEST_CODE_GESPLAYER_EXTERNAL_STORAGE);
        }
        else{
            //permissions have already been granted
        }

        //initiate connection to the MediaPlaybackService through MediaBrowser
        gesMediaBrowser = new MediaBrowserCompat(this,
                new ComponentName(this, MediaPlaybackService.class),
                mediaBrowserCallbacks, getIntent().getExtras());
        gesMediaBrowser.connect();

        //Attach listeners to them
        playPauseToggleButton.setOnClickListener(this);
        // space here for other buttons
        // sapce here for other buttons
    }

    @Override
    protected void onStart() {
        super.onStart();
        //gesMediaBrowser.connect();
    }

    /*protected void onStop() {
        super.onStop();
        // (see "stay in sync with the MediaSession")
        if( gesMediaController.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
            gesPlaybackController.pause();
        }
        gesMediaBrowser.disconnect();
    }*/
    @Override
    protected void onDestroy() {
        super.onDestroy();
        /*if (gesMediaController != null) {
            gesMediaController.unregisterCallback(mediaControllerCallbacks);
            gesMediaController = null;
        }
        if(gesMediaBrowser != null && gesMediaBrowser.isConnected()) {
            gesMediaBrowser.disconnect();
            gesMediaBrowser = null;
        }*/
        if( gesMediaController.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
            gesPlaybackController.pause();
        }

        gesMediaBrowser.disconnect();

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.playPause_btn:
                //has to be dealt with accordingly, based on the current state of mediaplayer
                int currentState = gesMediaController.getPlaybackState().getState();
                if(currentState == PlaybackStateCompat.STATE_PLAYING) {
                    gesPlaybackController.pause();
                } else {
                    //gesPlaybackController.play();
                    gesPlaybackController.playFromMediaId(String.valueOf(R.raw.warner_tautz_off_broadway), null);
                }
                break;
}

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_GESPLAYER_EXTERNAL_STORAGE: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, "onRequestPermissionResult: granted", Toast.LENGTH_SHORT).show();

                } else {
                    //close the app if permissions aren't granted
                    //Toast.makeText(this, "onRequestPermissionResult: denied", Toast.LENGTH_SHORT).show();
                    finish();
                }
                return;
            }
            // other 'case' lines to check for other
            // permissions this app might request.
        }
    }

And then the playbackService.java

public class MediaPlaybackService extends MediaBrowserServiceCompat implements
    MediaPlayer.OnCompletionListener,

    AudioManager.OnAudioFocusChangeListener {

public static final String COMMAND_EXAMPLE = "command_example";
public static boolean isServiceStarted = false;

/*public int audioIndex;
public ArrayList<Audio> audioList;
public Audio activeAudio;*/

private MediaPlayer gesMediaPlayer;
private MediaSessionCompat gesMediaSession;
private int pausedPosition;

//-------------------------------------Lifecycle methods--------------------------------------//

@Override
public void onCreate() {
    super.onCreate();

    Log.d("onCreate: ", "Service created");
    initMediaPlayer();
    initMediaSession();
    callStateListener();
    registerNoisyReceiver();

    // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
    PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder()
            .setActions(
                        PlaybackStateCompat.ACTION_PLAY |
                            PlaybackStateCompat.ACTION_PLAY_PAUSE);
    gesMediaSession.setPlaybackState(playbackStateBuilder.build());
}
@Override
public void onDestroy() {
    super.onDestroy();
    if (gesMediaPlayer != null) {
        gesMediaPlayer.stop();
        gesMediaPlayer.release();
    }

    //Disable the PhoneStateListener
    if (phoneStateListener != null) {
        telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
    }

    removeAudioFocus();
    unregisterReceiver(becomingNoisyReceiver);

    //clear cached playlist
    //new StorageUtils(getApplicationContext()).clearCachedAudioPlaylist();
    NotificationManagerCompat.from(this).cancel(1);
    stopSelf();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d("onStartCommand: ", "Service has been started");
    MediaButtonReceiver.handleIntent(gesMediaSession, intent);
    return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
    if( gesMediaPlayer != null ) {
        gesMediaPlayer.release();
    }
}

//----------------------------------------Initialisers----------------------------------------//

private void initMediaPlayer() {
    gesMediaPlayer = new MediaPlayer();
    gesMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
    gesMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    gesMediaPlayer.setVolume(1.0f, 1.0f);

    /*try {
        //sets songPath as data source for media player
        //gesMediaPlayer.setDataSource(songPath);

        //sets current song as data source for media player
        gesMediaPlayer.setDataSource(activeAudio.getData());
    } catch (IOException e) {
        e.printStackTrace();
        stopSelf();
    }
    gesMediaPlayer.prepareAsync();*/
}
private void initMediaSession() {
    ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
    gesMediaSession = new MediaSessionCompat(getApplicationContext(), "GESMediaService",
                                                mediaButtonReceiver, null);

    gesMediaSession.setCallback(mediaSessionCallbacks);
    gesMediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS );

    //this is for pre-Lollipop media button handling on those devices
    Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    mediaButtonIntent.setClass(this, MediaButtonReceiver.class);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);
    gesMediaSession.setMediaButtonReceiver(pendingIntent);

    // Set the session's token so that client activities can communicate with it.
    setSessionToken(gesMediaSession.getSessionToken());
}
private void registerNoisyReceiver() {
    //Handles headphones coming unplugged. cannot be done through a manifest receiver
    IntentFilter noisyFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
    registerReceiver(becomingNoisyReceiver, noisyFilter);
}
private void initMediaSessionMetadata() {
    MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();

    //Notification icon in card
    metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
    metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));

    //lock screen icon for pre lollipop
    metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
    metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Display Title");
    metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Display Subtitle");
    metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, 1);
    metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, 1);

    gesMediaSession.setMetadata(metadataBuilder.build());
}
//-----------------------------------Media Playback functions---------------------------------//

//TODO: read about the AssetFileDescriptor, and the ResultReceiver

private MediaSessionCompat.Callback mediaSessionCallbacks = new MediaSessionCompat.Callback() {
    @Override
    public void onPlay() {
        super.onPlay();
        if(!requestAudioFocus()) {
            //failed to gain focus
            return;
        }
        //check if service is started, not only bound
        if(!isServiceStarted){
            startService(new Intent(getApplicationContext(), MediaPlaybackService.class));
        }

        gesMediaSession.setActive(true);
        setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING);
        showPlayingNotification();
        gesMediaPlayer.start();
    }
    @Override
    public void onPause() {
        super.onPause();

        if( gesMediaPlayer.isPlaying() ) {
            gesMediaPlayer.pause();
            setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED);
            showPausedNotification();
        }
    }
    @Override
    public void onPlayFromMediaId(String mediaId, Bundle extras) {
        super.onPlayFromMediaId(mediaId, extras);

        try {
            AssetFileDescriptor afd = getResources().openRawResourceFd(Integer.valueOf(mediaId));
            if( afd == null ) {
                Log.d("afd: ", "afd in onPlayFromMediaId is null");
                return;
            }

            try {
                gesMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());

            } catch( IllegalStateException e ) {
                gesMediaPlayer.release();
                initMediaPlayer();
                gesMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            }

            afd.close();
            initMediaSessionMetadata();

        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        try {
            gesMediaPlayer.prepare();
        } catch (IOException e) {
            e.printStackTrace();
            Log.d("onPlayFromId: ", "mediaPlayer failed to prepare");
        }

        //Work with extras here if you want
    }
    @Override
    public void onCommand(String command, Bundle extras, ResultReceiver cb) {
        super.onCommand(command, extras, cb);
        if( COMMAND_EXAMPLE.equalsIgnoreCase(command) ) {
            //Custom command here
        }
    }
    @Override
    public void onSeekTo(long pos) {
        super.onSeekTo(pos);
    }
};

private void setMediaPlaybackState(int state) {
    PlaybackStateCompat.Builder playbackstateBuilder = new PlaybackStateCompat.Builder();
    if( state == PlaybackStateCompat.STATE_PLAYING ) {
        playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE |
                                        PlaybackStateCompat.ACTION_PAUSE);
    } else {
        playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE |
                                        PlaybackStateCompat.ACTION_PLAY);
    }
    playbackstateBuilder.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0);
    gesMediaSession.setPlaybackState(playbackstateBuilder.build());
}


//-------------------------------Audio Focus and Calls Handling-------------------------------//

//Handle incoming phone calls
private boolean ongoingCall = false;
private PhoneStateListener phoneStateListener;
private TelephonyManager telephonyManager;
private AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

@Override
public void onAudioFocusChange(int focusChange) {
    switch( focusChange ) {
        case AudioManager.AUDIOFOCUS_LOSS: {
            // Lost focus for an unbounded amount of time:
            // stop playback and release media player
            if( gesMediaPlayer.isPlaying() ) {
                gesMediaPlayer.stop();
            }
            /*gesMediaPlayer.release();
            gesMediaPlayer = null;*/
            break;
        }
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: {
            // Lost focus for a short time; Pause only and do not
            // release the media player as playback is likely to resume
            if (gesMediaPlayer.isPlaying()) {
                gesMediaPlayer.pause();
            }
            break;
        }
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
            // Lost focus for a short time (ex. notification sound)
            // but it's ok to keep playing at a temporarily attenuated level
            if( gesMediaPlayer != null ) {
                gesMediaPlayer.setVolume(0.2f, 0.2f);
            }
            break;
        }
        case AudioManager.AUDIOFOCUS_GAIN: {
            //Invoked when the audio focus of the system is updated.
            if( gesMediaPlayer != null ) {
                if( !gesMediaPlayer.isPlaying() ) {
                    gesMediaPlayer.start();
                }
                gesMediaPlayer.setVolume(1.0f, 1.0f);
            }
            break;
        }
    }
}
private boolean requestAudioFocus() {
    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    int result = audioManager.requestAudioFocus(this,
            AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN);
    return result == AudioManager.AUDIOFOCUS_GAIN;
}
private boolean removeAudioFocus() {
    return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            audioManager.abandonAudioFocus(this);
}
private void callStateListener() {
    // Get the telephony manager
    telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
    //Starting listening for PhoneState changes
    phoneStateListener = new PhoneStateListener() {
        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            switch (state) {
                //if at least one call exists or the phone is ringing
                //pause the MediaPlayer
                case TelephonyManager.CALL_STATE_OFFHOOK:
                case TelephonyManager.CALL_STATE_RINGING:
                    if (gesMediaPlayer != null && gesMediaPlayer.isPlaying()) {
                        gesMediaPlayer.pause();
                        pausedPosition = gesMediaPlayer.getCurrentPosition();
                        ongoingCall = true;
                    }
                    break;
                case TelephonyManager.CALL_STATE_IDLE:
                    // Phone idle. Start/resume playing.
                    if (gesMediaPlayer != null) {
                        if (ongoingCall) {
                            ongoingCall = false;
                            gesMediaPlayer.seekTo(pausedPosition);
                            gesMediaPlayer.start();
                        }
                    }
                    break;
            }
        }
    };
    // Register the listener with the telephony manager
    // Listen for changes to the device call state.
    telephonyManager.listen(phoneStateListener,
            PhoneStateListener.LISTEN_CALL_STATE);
}
private BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if( gesMediaPlayer != null && gesMediaPlayer.isPlaying() ) {
            gesMediaPlayer.pause();
        }
    }
};
//------------------------------------Less important methods----------------------------------//

@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
    if(TextUtils.equals(clientPackageName, getPackageName())) {
        return new BrowserRoot(getString(R.string.app_name), null);
    }

    return null;
}

//Not important for general audio service, required for class
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
    result.sendResult(null);
}

}

I apologize for the whole code but at this point I do not know what am I doing wrong anymore or where to look. Especially since the app did work in a previous version. Any suggestions are well appreciated if you guys see anything specific in my code. I try to catch exceptions where I can, but at the same time I am never sure where should I be putting the try-catch constructs. I'm trying to learn Thank you to anyone!

Emilian Cebuc
  • 331
  • 4
  • 20
  • 1
    `private AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);` - You cannot call `getSystemService()` in a field initializer. The `Context` it's ultimately called on has not yet been set. Move that into `onCreate()`, or a later lifecycle method. Also, please look into how to create a [mcve]. That's way too much code for this issue. In the future, please also include the stack trace in your question when asking for help debugging a crash. – Mike M. Mar 14 '18 at 22:26
  • 1
    thank you for all the suggestions. That did solve the issue – Emilian Cebuc Mar 14 '18 at 23:11

0 Answers0