[JAVA] How to make an crazy Android music player

I will make something like this

sc1.png Make a template for a very common music player.   cap2.gif

In addition, we will take a method that can support Wear and Auto without implementing dedicated processing.

There are no Japanese articles about Android music players. I made an article because I managed to implement it with various efforts. It will be longer, but please keep in touch if you like.

If you want to see only the implementation, skip to the implementation.

What a music player should look like on Android

When making a music player that runs on a PC, use various methods of instances such as MediaPlayer. I think the main way to make it was to call it by clicking the button pasted on the Form.

fig1.png

However, mobile platforms such as Android and iOS in recent years are not limited to smartphones. It has come to be incorporated into various devices such as watches, car audios, and televisions. Then, it became necessary to link with a smartphone, operate the player from a smart watch or car audio, and display song information. If a developer writes code for various devices (for Android Wear, Auto, TV, etc.), it will be a heavy burden. Therefore, there was a movement to standardize such processing in the Android series, and it was implemented in Android 5.0. That is MediaSession.

Position and role of Media Session

fig2.png

The figure above shows the mechanism around Android music. These are similar to server-client relationships.

The Media Session acts as a server. It provides the function to operate songs, player status, song information, etc. to connected clients.

The client requests an operation from the Media Session using Media Controller, Media Button Intent, etc. Displays the player status and song information to be notified.

reference: Understanding MediaSession (Part 1/4) Understanding MediaSession (Part 2/4)

Media Controller and Media Button Intent

Both are used to request the Media Session to operate the song (play, stop, skip, etc.). The difference is that Media Button Intent can only request operations, while Media Controller can also get player status and song information.

By the way, Media Session is assigned a unique ID called Session Token. If you use the server analogy above, it's like an IP address. In other words, if you know the Token, you can connect from the outside. When connecting, pass Token to the constructor of Media Controller.

Media Session implementation example

I think it's hard to imagine how to use Media Session from the above explanation, so I made a diagram. fig3-3.png

Looking at the figure, it looks like you're doing something quite annoying. However, it makes a lot of sense to take the above form when creating a music player on the Android platform.

I think that the music player you usually use will continue to play even if you remove Activity from recent tasks. Or even if you don't remove it from the task, moving to another app may destroy the Activity when it runs out of memory. In other words, if you leave some of the processing related to playback to Activity, you will not be able to continue playback when the Activity is destroyed.

Therefore, the important idea in creating a music player that runs on Android is to implement all the functions to play songs on the Service side, and on the Activity side, perform operations such as play and pause according to the user's operation Media Session. It is ** to clarify the division of roles **, such as simply displaying the song information being played from the Media Session on the UI.

You don't have to be afraid that the Activity will be destroyed by simply displaying the song information to the user and providing control to control the player. When the Activity is regenerated, you just need to reconnect to the Media Session.

Reference: Media Apps Overview

Media Session and Media Browser

In the previous example, Media Session was implemented in a normal Service. However, in practice it is recommended to implement it using Media Browser Service, which is an extension of Service.

The Media Session mentioned earlier acts like a server that provides song operations and information, and the Media Browser Service acts as a music library. If you implement it according to the specifications of Media Browser, you will be able to use functions such as automatically selecting songs from Wear and Auto. It also encapsulates operations such as binding and getting Session Token.

fig4.png It seems that there is not much difference from the precedent, as the names of steps ① and ② are easy to understand. In fact, a function to provide a list of songs will be added here.

reference: Building a Media Browser Service Building a Media Browser Client

Browsing the library according to the design concept of Media Browser

Next is about browsing the library. In short, it is a function that displays a list of songs that can be played to the user.

I told you the song you want to play in ④ of the previous example, but if you do not have a list of songs in the first place, you can not make a request. As mentioned above, Media Browser has a mechanism to provide a music library.

fig6.png

The figure above shows the process of connecting to the Media Browser Service and getting a list of songs. Call subscribe (MediaId) to request a Media Item from the Media Browser to the Media Browser Service. Then the list of Media Items is returned. Steps ③ and ④ are executed each time necessary.

Media ID concept

In Media Browser Service, all elements of songs, albums and genres are represented by the character string MediaId. And there is MediaBrowser.MediaItem in the class that holds the MediaId and the accompanying data.

The content held by MediaItem has a flag that identifies whether it has a child element, an item that has the role of a so-called folder, or a playable music item that has no children. See the figure below.

fig5.png

Orange is the Item that has the folder flag, and blue is the Item that has the playable flag. The written string indicates MediaId.

Don't get me wrong here, the folder-flagged MediaItem itself doesn't actually have child elements as objects. In the above figure, it is written like a tree structure, but there is no parent-child relationship in terms of objects.

I think it's hard to understand, so let's give a concrete example. Calling Media Browser subscribe (mediaId) will call ʻonLoadChildren ()of the connectedMedia Browser Serviceand will return the content sent from it. Initially callsubscribe ("root "). Then {"Genre "," Album "}will be returned. Suppose a user wants to be by genre. Then callsubscribe ("Genre "). Then {"Genre: Rock, "Genre: Jazz"}will be returned. Then go tosubscribe ("Genre: Rock ")and you'll get{" music1 "," music2 "}`. When the user selects music1, the Media Controller will request that the song with MediaId "music1" be played.

I wrote it as if music1 and music2 were automatically returned when I called subscribe ("Genre: Rock "), but I had to write the mechanism myself. For example, if "Genre:" is added to the beginning of MediaId, it is regarded as an ID that lists all songs of a specific genre, and the song is returned. In other words, you need to know what you are looking for with just the Media ID and return the song or subcategory.

However, in this sample, I simply make it feel like two songs are hanging from root.

List of classes that store song information

The classes prepared in advance are as follows.

name Description
MediaDescription A class that holds a minimum of metadata.
MediaMetadata ThesonginformationdeliveredfromMediaSessionisinthisformat.Inadditiontotheminimummetadata,youcansetcustomdatalikeanassociativearray.getDescription()You can also generate a Media Description with.
MediaBrowser.MediaItem The format for retrieving information from the MediaBrowserService. In addition to songs, items with child elements such as genres and albums can also be expressed. It has a Media Description inside.
MediaSession.QueueItem It has a Media Description inside and is used for information on songs in the queue. Therefore, index information is attached.

Why you can work with Wear and Auto

You can see that we use Media Browser and Media Controller to connect to Media Browser Service and Media Session. Actually, Wear and Auto also have these two. In the examples so far, it was connected from Activity, but Wear and Auto are just connected in the same way, so there is no need to write a dedicated process.

Cooperation example

cap3.gif Information on songs delivered from Media Session is displayed. Also, if you set a cue item in Media Session, a list of songs will be displayed below.

cap2.gif The Wear side also has MediaBrowser.subscribe, which gets a list of songs and displays them. (This time only songs, but depending on the implementation of onLoadChildren, menus by album or genre can also be displayed)

Audio Focus It's not a big deal given the presence of Media Sessions and Browsers, but it's also an important feature. ʻAudio Focus is the audio version of Focus in the UI. Make sure only one app has ʻAudio Focus. And if only apps with ʻAudio Focus` play songs, you can prevent multiple apps from playing music at the same time.

However, the Android system only notifies you when Focus hits or misses, so you write your own processing such as pausing when the focus is lost (such as when another app starts playing). is needed.

Implementation

This time, we will use the music data stored in the Assets folder. I felt that it would be complicated when the story of storage was involved, so I compromised by practicing using Media Session. The song metadata is also hard-coded. Please note.

** All code is here **

Target version

Since the notification channel is added in Android 8.0 and it will be necessary to branch depending on the version, this time the target is set to 25 and the minimum is set to 21. (We do not accept Tsukkomi that it is not imadoki ^^;)

Library used

MediaPlayer is fine because it only plays MP3s, but I use ExoPlayer that I usually use. ExoPlayer is a media player developed by Google and is designed to replace the default MediaPlayer, which may have different implementations depending on the version and model. As it is updated, the formats that can be played will increase, and it will also have functions such as live streaming. How to use ExoPlayer How to use ExoPlayer I think that will be helpful.

It also uses the Support Library. I also recommend the Google official because it absorbs the difference between versions. Therefore, use the one with Compat at the end, such as MediaSessionCompat. Dependent libraries are as follows

build.gradle


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support:design:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    implementation 'com.google.android.exoplayer:exoplayer:2.6.1'
}

Construction

image.png

・ Maina c Chity ty Activity displayed ・ Music Library I borrowed it from Google's official sample. The song information is hard-coded. ・ MusicService Implementation of Meida Browser Service.

Below is a diagram of the relationship between callbacks and methods. fig9.png

AndroidManifest.xml

AndroidManifest.xml


    <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=".MusicService">
            <intent-filter>
                <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"/>
            </intent-filter>
        </receiver>
    </application>

This is an excerpt from the Application part. Register MusicService and also set the receiver to receive MediaButtonIntent.

MainActivity UI fig7.png   ** Click here for UI xml file (https://github.com/SIY1121/MediaSessionSample/blob/master/app/src/main/res/layout/activity_main.xml) **

Initialize & connect

MainActivity.java


    MediaBrowserCompat mBrowser;
    MediaControllerCompat mController;

    //UI related is omitted

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

        //UI setup (omitted)

        //Keep the service running
        //This is unnecessary if Service can be stopped at the same time as Activity is destroyed.
        startService(new Intent(this, MusicService.class));

        //Initialize MediaBrowser
        mBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class), connectionCallback, null);
        //Connect(Bind service)
        mBrowser.connect();
    }

    //Callback called when connecting
    private MediaBrowserCompat.ConnectionCallback connectionCallback = new MediaBrowserCompat.ConnectionCallback() {
        @Override
        public void onConnected() {
            try {
                //Since Session Token can be obtained when the connection is completed
                //Create a Media Controller using it
                mController = new MediaControllerCompat(MainActivity.this, mBrowser.getSessionToken());
                //Set a callback when the player status or song information sent from the service is changed
                mController.registerCallback(controllerCallback);

                //If it is already playing, call the callback itself and update the UI
                if (mController.getPlaybackState() != null && mController.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING) {
                    controllerCallback.onMetadataChanged(mController.getMetadata());
                    controllerCallback.onPlaybackStateChanged(mController.getPlaybackState());
                }


            } catch (RemoteException ex) {
                ex.printStackTrace();
                Toast.makeText(MainActivity.this, ex.getMessage(), Toast.LENGTH_LONG).show();
            }
            //Get a list of playable songs from the service
            mBrowser.subscribe(mBrowser.getRoot(), subscriptionCallback);
        }
    };

    //Callback that is called when you subscribe
    private MediaBrowserCompat.SubscriptionCallback subscriptionCallback = new MediaBrowserCompat.SubscriptionCallback() {
        @Override
        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaBrowserCompat.MediaItem> children) {
            //Request to play the first song if it is not already playing
            if (mController.getPlaybackState() == null)
                Play(children.get(0).getMediaId());
        }
    };

First, start Music Service.

After that, MediaBrowser is initialized and connected. When the connection is completed, ʻonConnected ()is called, so get the Token withmBrowser.getSessionToken ()and pass it to the constructor ofMediaController` to connect with Media Session.

Then call mBrowser.subscribe (mBrowser.getRoot (), subscriptionCallback) to request a list of songs. When the Service side sends a list, the set callback ʻonChildrenLoaded ()` is called, so this time we will play the first item.

Communicate with Media Session using Media Controller

MainActivity.java


    private void Play(String id) {
        //Get TransportControl to request operation from MediaController to service
        //When playFromMediaId is called, onPlayFromMediaId in the callback of MediaSession on the service side is called.
        mController.getTransportControls().playFromMediaId(id, null);
    }

You can get the TransportControl used to request operations from the MediaSession withmController.getTransportControls ().

Processing when information is delivered from MediaSession

MainActivity.java


    //MediaController callback
    private MediaControllerCompat.Callback controllerCallback = new MediaControllerCompat.Callback() {
        //Called when the information of the song being played is changed
        @Override
        public void onMetadataChanged(MediaMetadataCompat metadata) {
            textView_title.setText(metadata.getDescription().getTitle());
            imageView.setImageBitmap(metadata.getDescription().getIconBitmap());
            textView_duration.setText(Long2TimeString(metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)));
            seekBar.setMax((int) metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION));
        }

        //Called when the player's state changes
        @Override
        public void onPlaybackStateChanged(PlaybackStateCompat state) {

            //Change button behavior and icon depending on player status
            if (state.getState() == PlaybackStateCompat.STATE_PLAYING) {
                button_play.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mController.getTransportControls().pause();
                    }
                });
                button_play.setImageResource(R.drawable.exo_controls_pause);
            } else {
                button_play.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mController.getTransportControls().play();
                    }
                });
                button_play.setImageResource(R.drawable.exo_controls_play);
            }

            textView_position.setText(Long2TimeString(state.getPosition()));
            seekBar.setProgress((int) state.getPosition());

        }
    };

When the state of the player or the song to be played changes, information is sent from MediaSession and the callback of MediaController is called. Reflect the change in the UI.

MusicService Create a Service that inherits from MediaBrowserService.

Processing when connected from a client

MusicService.java


    //Called when connecting to a client
    //Decide whether to connect from the package name etc.
    //Connection permission when an arbitrary character string is returned
    //Deny connection with null
    //Allow all connections this time
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName,
                                 int clientUid,
                                 Bundle rootHints) {
        Log.d(TAG, "Connected from pkg:" + clientPackageName + " uid:" + clientUid);
        return new BrowserRoot(ROOT_ID, null);
    }

    //Called when the client calls subscribe
    //Returns the contents of the music library
    //Also used for the list of songs displayed in Wear and Auto
    //By default, the string returned by onGetRoot is passed to parentMediaId
    //The Id is also passed when you select a MediaItem that has child elements on the browser screen.
    @Override
    public void onLoadChildren(
            @NonNull final String parentMediaId,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        if (parentMediaId.equals(ROOT_ID))
            //Send a list of songs to the client
            result.sendResult(MusicLibrary.getMediaItems());
        else//This time ROOT_Invalid except ID
            result.sendResult(new ArrayList<MediaBrowserCompat.MediaItem>());
    }

ʻOnGetRoot () is called when connecting and ʻonLoadChildren () is called when subscribed. Since ʻonLoadChildren ()is supposed to take time, it takes the form of sending the result to the result object instead of returning it with return. If you want to return it immediately, putresult.sendResult, and if you want to return it asynchronously later, put result.detatch () `.

Initialization

MusicService.java


    final String TAG = MusicService.class.getSimpleName();//Log tag
    final String ROOT_ID = "root";//ID onGetRoot to return to client/Used in onLoadChildren

    Handler handler;//Handler for turning processing on a regular basis

    MediaSessionCompat mSession;//Media Session of the leading role
    AudioManager am;//Manager for handling AudioFoucs

    int index = 0;//Index being played

    ExoPlayer exoPlayer;//The substance of the music player

    List<MediaSessionCompat.QueueItem> queueItems = new ArrayList<>();//List to use for queue

    @Override
    public void onCreate() {
        super.onCreate();
        //Get AudioManager
        am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        //Initialize MediaSession
        mSession = new MediaSessionCompat(getApplicationContext(), TAG);
        //Set the functions provided by this Media Session
        mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | //Handle buttons such as headphones
                MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS | //Supports the use of queue commands
                MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); //Provides controls for play, stop, skip, etc.

        //Set callback according to the operation from the client
        mSession.setCallback(callback);

        //Set Session Token in MediaBrowserService
        setSessionToken(mSession.getSessionToken());

        //When the Media Session metadata and player status are updated
        //Create notification/Update
        mSession.getController().registerCallback(new MediaControllerCompat.Callback() {
            @Override
            public void onPlaybackStateChanged(PlaybackStateCompat state) {
                CreateNotification();
            }

            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) {
                CreateNotification();
            }
        });


        //Add item to queue
        int i = 0;
        for (MediaBrowserCompat.MediaItem media : MusicLibrary.getMediaItems()) {
            queueItems.add(new MediaSessionCompat.QueueItem(media.getDescription(), i));
            i++;
        }
        mSession.setQueue(queueItems);//Queues are displayed in Wear and Auto


        //Initialization of exoPlayer
        exoPlayer = ExoPlayerFactory.newSimpleInstance(getApplicationContext(), new DefaultTrackSelector());
        //Set player event listener
        exoPlayer.addListener(eventListener);

        handler = new Handler();
        //Update playback information every 500ms
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //Update during playback
                if (exoPlayer.getPlaybackState() == Player.STATE_READY && exoPlayer.getPlayWhenReady())
                    UpdatePlaybackState();

                //Run again
                handler.postDelayed(this, 500);
            }
        }, 500);
    }

Since the amount is large, I will explain it briefly. First of all, about mSession.setFlags () which is done after initializing MediaSession. You can enable the Media Session feature by setting a flag. Please note that no function can be used unless it is set in reverse.

Then setSessionToken (mSession.getSessionToken ()) This sets the value returned by getSessionToken () of the client (MediaBrowser).

Also, since ExoPlayer will be used this time, we will initialize ExoPlayer.

Finally, the handler is used to call ʻUpdatePlaybackState ()` at regular intervals. This process will be explained later.

Handle requests for Media Session from clients

This is the part that is paired with Transport Control that was used to request operations to MediaSession in MainActivity. fig8.png

MusicService.java


    //Callback for Media Session
    private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {

        //Play from song ID
        //This is also called when a song is selected from the Wear or Auto browsing screen.
        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            //This time play the audio file contained in the Assets folder
            //Play from Uri
            DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(), Util.getUserAgent(getApplicationContext(), "AppName"));
            MediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse("file:///android_asset/" + MusicLibrary.getMusicFilename(mediaId)));

            //This time, the index is easily calculated from the mediaId.
            for (MediaSessionCompat.QueueItem item : queueItems)
                if (item.getDescription().getMediaId().equals(mediaId))
                    index = (int) item.getQueueId();

            exoPlayer.prepare(mediaSource);

            mSession.setActive(true);

            onPlay();

            //Set information about the song being played, delivered by MediaSession
            mSession.setMetadata(MusicLibrary.getMetadata(getApplicationContext(), mediaId));
        }

        //When requested to play
        @Override
        public void onPlay() {
            //Request audio focus
            if (am.requestAudioFocus(afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                //If you can get it, start playing
                mSession.setActive(true);
                exoPlayer.setPlayWhenReady(true);
            }
        }

        //When a pause is requested
        @Override
        public void onPause() {
            exoPlayer.setPlayWhenReady(false);
            //Release audio focus
            am.abandonAudioFocus(afChangeListener);
        }

        //When requested to stop
        @Override
        public void onStop() {
            onPause();
            mSession.setActive(false);
            //Release audio focus
            am.abandonAudioFocus(afChangeListener);
        }

        //When a seek is requested
        @Override
        public void onSeekTo(long pos) {
            exoPlayer.seekTo(pos);
        }

        //When the next song is requested
        @Override
        public void onSkipToNext() {
            index++;
            if (index >= MusicLibrary.getMediaItems().size())//After playing to the end of the library
                index = 0;//Return to the beginning

            onPlayFromMediaId(queueItems.get(index).getDescription().getMediaId(), null);
        }

        //When the previous song is requested
        @Override
        public void onSkipToPrevious() {
            index--;
            if (index < 0)//When the index goes below 0
                index = queueItems.size() - 1;//Move to the last song

            onPlayFromMediaId(queueItems.get(index).getDescription().getMediaId(), null);
        }

        //Also called when an item in the queue is selected with Wear or Auto
        @Override
        public void onSkipToQueueItem(long i) {
            onPlayFromMediaId(queueItems.get((int)i).getDescription().getMediaId(), null);
        }

        //Called when the Media Button Intent flies
        //No override required (just spit out logs this time)
        //The operations that can be performed change according to the Action flag of playbackState of MediaSession.
        @Override
        public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
            KeyEvent key = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            Log.d(TAG, String.valueOf(key.getKeyCode()));
            return super.onMediaButtonEvent(mediaButtonEvent);
        }
    };

There are two points to note here.

First of all, mSession.setMetadata () which is the final process of ʻonPlayFromMediaId () If you set Metadata in Media Session, song information will be delivered to connected clients. Taking MainActivity as an example, the MediaController callback ʻonMetadataChanged () is called.

Then ʻonMediaButtonEvent ()` This is the place that will be called when the Media Button Intent flies. It also includes physical buttons on wired headphones and controls from Bluetooth-connected devices.

Unlike other methods, there is already an implementation here, so there is basically no need to override it. For example, processing such as mapping a single physical button press on headphones to onPlay () and onPause () and a double press to onSkipNext () has already been implemented.

However, if you want to add your own operation by pressing the physical button, you will handle it here.

Notify the client of the player's status

MusicService.java


    //Player callback
    private Player.EventListener eventListener = new Player.DefaultEventListener() {
        //Called when the player's status changes
        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            UpdatePlaybackState();
        }
    };

    //Set the current player status delivered by MediaSession
    //This includes information on the playback position, so update it regularly.
    private void UpdatePlaybackState() {
        int state = PlaybackStateCompat.STATE_NONE;
        //Set the appropriate Media Session status from the player's status
        switch (exoPlayer.getPlaybackState()) {
            case Player.STATE_IDLE:
                state = PlaybackStateCompat.STATE_NONE;
                break;
            case Player.STATE_BUFFERING:
                state = PlaybackStateCompat.STATE_BUFFERING;
                break;
            case Player.STATE_READY:
                if (exoPlayer.getPlayWhenReady())
                    state = PlaybackStateCompat.STATE_PLAYING;
                else
                    state = PlaybackStateCompat.STATE_PAUSED;
                break;
            case Player.STATE_ENDED:
                state = PlaybackStateCompat.STATE_STOPPED;
                break;
        }

        //Set player information, current playback position, etc.
        //Also, set the operations that can be performed with MeidaButtonIntent.
        mSession.setPlaybackState(new PlaybackStateCompat.Builder()
                .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_STOP)
                .setState(state, exoPlayer.getCurrentPosition(), exoPlayer.getPlaybackParameters().speed)
                .build());
    }

The first callback is from ExoPlayer. As the name implies, it is called when the status of Player changes.

ʻUpdatePlaybackState ()` sets the state of the player delivered by MediaSession. The contents to be delivered are the status such as Playing and Paused, the playback position, the playback speed, and the currently accepted operations. The status PlaybackStateCompat.STATE_XXXX, which indicates the state of the player, and the status of ExoPlayer are different and must be converted to the corresponding ones. By the way, the fact that the playback position is included means that the playback position must always be delivered. Therefore, I am trying to call here every 0.5 seconds in the initialization (onCreate) part.

Create notification

MusicService.java


    //Create a notification and make the service Foreground
    private void CreateNotification() {
        MediaControllerCompat controller = mSession.getController();
        MediaMetadataCompat mediaMetadata = controller.getMetadata();

        if (mediaMetadata == null && !mSession.isActive()) return;

        MediaDescriptionCompat description = mediaMetadata.getDescription();

        NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext());

        builder
                //Set information for the current song
                .setContentTitle(description.getTitle())
                .setContentText(description.getSubtitle())
                .setSubText(description.getDescription())
                .setLargeIcon(description.getIconBitmap())

                //Set intent when clicking notifications
                .setContentIntent(createContentIntent())

                //Set intent when notifications are swiped off
                .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this,
                        PlaybackStateCompat.ACTION_STOP))

                //Make the notification range public so that it will be displayed on the lock screen
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

                .setSmallIcon(R.drawable.exo_controls_play)
                //Set the color used for the notification area
                //Styles change depending on Android version, and colors are often not applied
                .setColor(ContextCompat.getColor(this, R.color.colorAccent))

                //Use Media Style
                .setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
                        .setMediaSession(mSession.getSessionToken())
                        //Set the index of the control that is displayed when the notification is folded small
                        .setShowActionsInCompactView(1));

        // Android4.Before 4, you can't swipe to dismiss notifications
        //Deal with by displaying the cancel button
        //This time the min SDK is 21, so it's not necessary
        //.setShowCancelButton(true)
        //.setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this,
        //        PlaybackStateCompat.ACTION_STOP)));

        //Notification control settings
        builder.addAction(new NotificationCompat.Action(
                R.drawable.exo_controls_previous, "prev",
                MediaButtonReceiver.buildMediaButtonPendingIntent(this,
                        PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)));

        //Set play and pause buttons in the player state
        if (controller.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING) {
            builder.addAction(new NotificationCompat.Action(
                    R.drawable.exo_controls_pause, "pause",
                    MediaButtonReceiver.buildMediaButtonPendingIntent(this,
                            PlaybackStateCompat.ACTION_PAUSE)));
        } else {
            builder.addAction(new NotificationCompat.Action(
                    R.drawable.exo_controls_play, "play",
                    MediaButtonReceiver.buildMediaButtonPendingIntent(this,
                            PlaybackStateCompat.ACTION_PLAY)));
        }


        builder.addAction(new NotificationCompat.Action(
                R.drawable.exo_controls_next, "next",
                MediaButtonReceiver.buildMediaButtonPendingIntent(this,
                        PlaybackStateCompat.ACTION_SKIP_TO_NEXT)));

        startForeground(1, builder.build());

        //Allow swipes to turn off notifications when not playing
        if (controller.getPlaybackState().getState() != PlaybackStateCompat.STATE_PLAYING)
            stopForeground(false);
    }

To create a notification with playback control, you need to specify MediaStyle with setStyle and set Media Button Intent with addAction to raise it.

Handle Audio Focus

MusicService.java


    //Audio focus callback
    AudioManager.OnAudioFocusChangeListener afChangeListener =
            new AudioManager.OnAudioFocusChangeListener() {
                public void onAudioFocusChange(int focusChange) {
                    //If you lose focus completely
                    if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                        //stop
                        mSession.getController().getTransportControls().pause();
                    } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {//Temporary focus lost
                        //stop
                        mSession.getController().getTransportControls().pause();
                    } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {//Focus lost due to notification sound (should be turned down and continue playing)
                        //Normally you should turn down the volume temporarily, but do nothing
                    } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {//When the focus is regained
                        //Regeneration
                        mSession.getController().getTransportControls().play();
                    }
                }
            };

We have implemented a mechanism to stop playback if another app starts playing sound and the Audio Focus of this app is lost.

Impressions

I felt that it was very well done because there was only an API implemented from Android 5.0.

Since many Android music players have already been released, I think that there are few opportunities to make a particularly pure music player by yourself, but it may be useful if you have the opportunity to play music in the background such as internet radio. Hmm.

I tried to explain Android Media API in one article. It's been a long time and I'm surprised myself. In such a case, is it easier to read by dividing it?

Anyway, thank you for watching until the end.

Google official sample

-Android MediaBrowserService Sample This was simplified and this sample was created. ・ Media Controller Test A sample that implements Media Browser and Media Controller and can control other music apps. ・ Universal Android Music Player Sample A fairly neat music player implementation. You are using the browsing function of Media Browser Service properly.

Recommended Posts

How to make an crazy Android music player
[Android] How to make Dialog Fragment
How to make an app using Tensorflow with Android Studio
How to make shaded-jar
How to make Unity Native Plugin (Android version)
Try to make a music player using Basic Player
How to make an oleore generator using swagger codegen
How to make an almost static page with rails
How to create an application
Java --How to make JTable
Make an android app. (Day 5)
How to handle an instance
[Rails] How to make seed
[2020 version] How to send an email using Android Studio Javamail
What is an immutable object? [Explanation of how to make]
How to make a Java container
How to make a JDBC driver
Make an android app. (First day)
How to insert an external library
How to make a Jenkins plugin
How to make a Maven project
How to make a Java array
How to make an image posted using css look like an icon
I tried to make an Android application with MVC now (Java)
How to crop an image with libGDX
Make an executable jar using Android Studio
How to make a Java calendar Summary
How to blur an image (super easy)
I want to make an ios.android app
[Android] How to deal with dark themes
How to detect microphone conflicts on Android
How to make a Discord bot (Java)
How to define an inner class bean
How to make asynchronous pagenations using Kaminari
How to write an RSpec controller test
How to make an app with a plugin mechanism [C # and Java]
How to use ExpandableListView in Android Studio
How to make a judgment method to search for an arbitrary character in an array
I'm making an Android app and I'm stuck with errors and how to solve it
[Android] How to convert a character string to resourceId
How to make rbenv recognize OpenSSL on WSL
How to write an if statement to improve readability-java
[Android] How to detect volume change (= volume button press)
How to use an array for HashMap keys
How to make Spring Boot Docker Image smaller
How to play audio and music using javascript
Make software that mirrors Android screen to PC 1
How to make duplicate check logic more readable
How to make a lightweight JRE for distribution
Rails6.0 ~ How to create an eco-friendly development environment
How to solve an Expression Problem in Java
How to write React Native bridge ~ Android version ~
[Rails] How to build an environment with Docker
How to make a follow function in Rails
How to create an oleore certificate (SSL certificate, self-signed certificate)
[Java] How to make multiple for loops single
How to install Ruby on an EC2 instance on AWS
How to make batch processing with Rails + Heroku configuration
How to make a factory with a model with polymorphic association
[Android Studio] How to change TextView to any font [Java]
[Swift] How to play songs from your music library