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 }