Home | History | Annotate | Download | only in client
      1 /*
      2  * Copyright 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.mediasession.client;
     18 
     19 import android.content.ComponentName;
     20 import android.content.Context;
     21 import android.os.RemoteException;
     22 import android.support.annotation.NonNull;
     23 import android.support.annotation.Nullable;
     24 import android.support.v4.media.MediaBrowserCompat;
     25 import android.support.v4.media.MediaMetadataCompat;
     26 import android.support.v4.media.session.MediaControllerCompat;
     27 import android.support.v4.media.session.MediaSessionCompat;
     28 import android.support.v4.media.session.PlaybackStateCompat;
     29 import android.util.Log;
     30 
     31 import com.example.android.mediasession.service.MusicService;
     32 
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 
     36 /**
     37  * Adapter for a MediaBrowser that handles connecting, disconnecting,
     38  * and basic browsing.
     39  */
     40 public class MediaBrowserAdapter {
     41 
     42     private static final String TAG = MediaBrowserAdapter.class.getSimpleName();
     43 
     44     /**
     45      * Helper class for easily subscribing to changes in a MediaBrowserService connection.
     46      */
     47     public static abstract class MediaBrowserChangeListener {
     48 
     49         public void onConnected(@Nullable MediaControllerCompat mediaController) {
     50         }
     51 
     52         public void onMetadataChanged(@Nullable MediaMetadataCompat mediaMetadata) {
     53         }
     54 
     55         public void onPlaybackStateChanged(@Nullable PlaybackStateCompat playbackState) {
     56         }
     57     }
     58 
     59     private final InternalState mState;
     60 
     61     private final Context mContext;
     62     private final List<MediaBrowserChangeListener> mListeners = new ArrayList<>();
     63 
     64     private final MediaBrowserConnectionCallback mMediaBrowserConnectionCallback =
     65             new MediaBrowserConnectionCallback();
     66     private final MediaControllerCallback mMediaControllerCallback =
     67             new MediaControllerCallback();
     68     private final MediaBrowserSubscriptionCallback mMediaBrowserSubscriptionCallback =
     69             new MediaBrowserSubscriptionCallback();
     70 
     71     private MediaBrowserCompat mMediaBrowser;
     72 
     73     @Nullable
     74     private MediaControllerCompat mMediaController;
     75 
     76     public MediaBrowserAdapter(Context context) {
     77         mContext = context;
     78         mState = new InternalState();
     79     }
     80 
     81     public void onStart() {
     82         if (mMediaBrowser == null) {
     83             mMediaBrowser =
     84                     new MediaBrowserCompat(
     85                             mContext,
     86                             new ComponentName(mContext, MusicService.class),
     87                             mMediaBrowserConnectionCallback,
     88                             null);
     89             mMediaBrowser.connect();
     90         }
     91         Log.d(TAG, "onStart: Creating MediaBrowser, and connecting");
     92     }
     93 
     94     public void onStop() {
     95         if (mMediaController != null) {
     96             mMediaController.unregisterCallback(mMediaControllerCallback);
     97             mMediaController = null;
     98         }
     99         if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
    100             mMediaBrowser.disconnect();
    101             mMediaBrowser = null;
    102         }
    103         resetState();
    104         Log.d(TAG, "onStop: Releasing MediaController, Disconnecting from MediaBrowser");
    105     }
    106 
    107     /**
    108      * The internal state of the app needs to revert to what it looks like when it started before
    109      * any connections to the {@link MusicService} happens via the {@link MediaSessionCompat}.
    110      */
    111     private void resetState() {
    112         mState.reset();
    113         performOnAllListeners(new ListenerCommand() {
    114             @Override
    115             public void perform(@NonNull MediaBrowserChangeListener listener) {
    116                 listener.onPlaybackStateChanged(null);
    117             }
    118         });
    119         Log.d(TAG, "resetState: ");
    120     }
    121 
    122     public MediaControllerCompat.TransportControls getTransportControls() {
    123         if (mMediaController == null) {
    124             Log.d(TAG, "getTransportControls: MediaController is null!");
    125             throw new IllegalStateException();
    126         }
    127         return mMediaController.getTransportControls();
    128     }
    129 
    130     public void addListener(MediaBrowserChangeListener listener) {
    131         if (listener != null) {
    132             mListeners.add(listener);
    133         }
    134     }
    135 
    136     public void removeListener(MediaBrowserChangeListener listener) {
    137         if (listener != null) {
    138             if (mListeners.contains(listener)) {
    139                 mListeners.remove(listener);
    140             }
    141         }
    142     }
    143 
    144     public void performOnAllListeners(@NonNull ListenerCommand command) {
    145         for (MediaBrowserChangeListener listener : mListeners) {
    146             if (listener != null) {
    147                 try {
    148                     command.perform(listener);
    149                 } catch (Exception e) {
    150                     removeListener(listener);
    151                 }
    152             }
    153         }
    154     }
    155 
    156     public interface ListenerCommand {
    157 
    158         void perform(@NonNull MediaBrowserChangeListener listener);
    159     }
    160 
    161     // Receives callbacks from the MediaBrowser when it has successfully connected to the
    162     // MediaBrowserService (MusicService).
    163     public class MediaBrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
    164 
    165         // Happens as a result of onStart().
    166         @Override
    167         public void onConnected() {
    168             try {
    169                 // Get a MediaController for the MediaSession.
    170                 mMediaController = new MediaControllerCompat(mContext,
    171                                                              mMediaBrowser.getSessionToken());
    172                 mMediaController.registerCallback(mMediaControllerCallback);
    173 
    174                 // Sync existing MediaSession state to the UI.
    175                 mMediaControllerCallback.onMetadataChanged(
    176                         mMediaController.getMetadata());
    177                 mMediaControllerCallback
    178                         .onPlaybackStateChanged(mMediaController.getPlaybackState());
    179 
    180                 performOnAllListeners(new ListenerCommand() {
    181                     @Override
    182                     public void perform(@NonNull MediaBrowserChangeListener listener) {
    183                         listener.onConnected(mMediaController);
    184                     }
    185                 });
    186             } catch (RemoteException e) {
    187                 Log.d(TAG, String.format("onConnected: Problem: %s", e.toString()));
    188                 throw new RuntimeException(e);
    189             }
    190 
    191             mMediaBrowser.subscribe(mMediaBrowser.getRoot(), mMediaBrowserSubscriptionCallback);
    192         }
    193     }
    194 
    195     // Receives callbacks from the MediaBrowser when the MediaBrowserService has loaded new media
    196     // that is ready for playback.
    197     public class MediaBrowserSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
    198 
    199         @Override
    200         public void onChildrenLoaded(@NonNull String parentId,
    201                                      @NonNull List<MediaBrowserCompat.MediaItem> children) {
    202             assert mMediaController != null;
    203 
    204             // Queue up all media items for this simple sample.
    205             for (final MediaBrowserCompat.MediaItem mediaItem : children) {
    206                 mMediaController.addQueueItem(mediaItem.getDescription());
    207             }
    208 
    209             // Call "playFromMedia" so the UI is updated.
    210             mMediaController.getTransportControls().prepare();
    211         }
    212     }
    213 
    214     // Receives callbacks from the MediaController and updates the UI state,
    215     // i.e.: Which is the current item, whether it's playing or paused, etc.
    216     public class MediaControllerCallback extends MediaControllerCompat.Callback {
    217 
    218         @Override
    219         public void onMetadataChanged(final MediaMetadataCompat metadata) {
    220             // Filtering out needless updates, given that the metadata has not changed.
    221             if (isMediaIdSame(metadata, mState.getMediaMetadata())) {
    222                 Log.d(TAG, "onMetadataChanged: Filtering out needless onMetadataChanged() update");
    223                 return;
    224             } else {
    225                 mState.setMediaMetadata(metadata);
    226             }
    227             performOnAllListeners(new ListenerCommand() {
    228                 @Override
    229                 public void perform(@NonNull MediaBrowserChangeListener listener) {
    230                     listener.onMetadataChanged(metadata);
    231                 }
    232             });
    233         }
    234 
    235         @Override
    236         public void onPlaybackStateChanged(@Nullable final PlaybackStateCompat state) {
    237             mState.setPlaybackState(state);
    238             performOnAllListeners(new ListenerCommand() {
    239                 @Override
    240                 public void perform(@NonNull MediaBrowserChangeListener listener) {
    241                     listener.onPlaybackStateChanged(state);
    242                 }
    243             });
    244         }
    245 
    246         // This might happen if the MusicService is killed while the Activity is in the
    247         // foreground and onStart() has been called (but not onStop()).
    248         @Override
    249         public void onSessionDestroyed() {
    250             resetState();
    251             onPlaybackStateChanged(null);
    252             Log.d(TAG, "onSessionDestroyed: MusicService is dead!!!");
    253         }
    254 
    255         private boolean isMediaIdSame(MediaMetadataCompat currentMedia,
    256                                      MediaMetadataCompat newMedia) {
    257             if (currentMedia == null || newMedia == null) {
    258                 return false;
    259             }
    260             String newMediaId =
    261                     newMedia.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
    262             String currentMediaId =
    263                     currentMedia.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
    264             return newMediaId.equals(currentMediaId);
    265         }
    266 
    267     }
    268 
    269     // A holder class that contains the internal state.
    270     public class InternalState {
    271 
    272         private PlaybackStateCompat playbackState;
    273         private MediaMetadataCompat mediaMetadata;
    274 
    275         public void reset() {
    276             playbackState = null;
    277             mediaMetadata = null;
    278         }
    279 
    280         public PlaybackStateCompat getPlaybackState() {
    281             return playbackState;
    282         }
    283 
    284         public void setPlaybackState(PlaybackStateCompat playbackState) {
    285             this.playbackState = playbackState;
    286         }
    287 
    288         public MediaMetadataCompat getMediaMetadata() {
    289             return mediaMetadata;
    290         }
    291 
    292         public void setMediaMetadata(MediaMetadataCompat mediaMetadata) {
    293             this.mediaMetadata = mediaMetadata;
    294         }
    295     }
    296 
    297 }