1 /* 2 * Copyright (C) 2020 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.android.systemui.media; 18 19 import android.annotation.Nullable; 20 import android.app.PendingIntent; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.media.MediaDescription; 26 import android.media.browse.MediaBrowser; 27 import android.media.session.MediaController; 28 import android.media.session.MediaSession; 29 import android.os.Bundle; 30 import android.service.media.MediaBrowserService; 31 import android.text.TextUtils; 32 import android.util.Log; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 36 import java.util.List; 37 38 /** 39 * Media browser for managing resumption in media controls 40 */ 41 public class ResumeMediaBrowser { 42 43 /** Maximum number of controls to show on boot */ 44 public static final int MAX_RESUMPTION_CONTROLS = 5; 45 46 /** Delimiter for saved component names */ 47 public static final String DELIMITER = ":"; 48 49 private static final String TAG = "ResumeMediaBrowser"; 50 private final Context mContext; 51 @Nullable private final Callback mCallback; 52 private MediaBrowserFactory mBrowserFactory; 53 private MediaBrowser mMediaBrowser; 54 private ComponentName mComponentName; 55 56 /** 57 * Initialize a new media browser 58 * @param context the context 59 * @param callback used to report media items found 60 * @param componentName Component name of the MediaBrowserService this browser will connect to 61 */ ResumeMediaBrowser(Context context, @Nullable Callback callback, ComponentName componentName, MediaBrowserFactory browserFactory)62 public ResumeMediaBrowser(Context context, @Nullable Callback callback, 63 ComponentName componentName, MediaBrowserFactory browserFactory) { 64 mContext = context; 65 mCallback = callback; 66 mComponentName = componentName; 67 mBrowserFactory = browserFactory; 68 } 69 70 /** 71 * Connects to the MediaBrowserService and looks for valid media. If a media item is returned, 72 * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription. 73 * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be 74 * called when the initial connection is successful, or an error occurs. 75 * Note that it is possible for the service to connect but for no playable tracks to be found. 76 * ResumeMediaBrowser#disconnect will be called automatically with this function. 77 */ findRecentMedia()78 public void findRecentMedia() { 79 Log.d(TAG, "Connecting to " + mComponentName); 80 disconnect(); 81 Bundle rootHints = new Bundle(); 82 rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); 83 mMediaBrowser = mBrowserFactory.create( 84 mComponentName, 85 mConnectionCallback, 86 rootHints); 87 mMediaBrowser.connect(); 88 } 89 90 private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = 91 new MediaBrowser.SubscriptionCallback() { 92 @Override 93 public void onChildrenLoaded(String parentId, 94 List<MediaBrowser.MediaItem> children) { 95 if (children.size() == 0) { 96 Log.d(TAG, "No children found for " + mComponentName); 97 if (mCallback != null) { 98 mCallback.onError(); 99 } 100 } else { 101 // We ask apps to return a playable item as the first child when sending 102 // a request with EXTRA_RECENT; if they don't, no resume controls 103 MediaBrowser.MediaItem child = children.get(0); 104 MediaDescription desc = child.getDescription(); 105 if (child.isPlayable() && mMediaBrowser != null) { 106 if (mCallback != null) { 107 mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), 108 ResumeMediaBrowser.this); 109 } 110 } else { 111 Log.d(TAG, "Child found but not playable for " + mComponentName); 112 if (mCallback != null) { 113 mCallback.onError(); 114 } 115 } 116 } 117 disconnect(); 118 } 119 120 @Override 121 public void onError(String parentId) { 122 Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId); 123 if (mCallback != null) { 124 mCallback.onError(); 125 } 126 disconnect(); 127 } 128 129 @Override 130 public void onError(String parentId, Bundle options) { 131 Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId 132 + ", options: " + options); 133 if (mCallback != null) { 134 mCallback.onError(); 135 } 136 disconnect(); 137 } 138 }; 139 140 private final MediaBrowser.ConnectionCallback mConnectionCallback = 141 new MediaBrowser.ConnectionCallback() { 142 /** 143 * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed. 144 * For resumption controls, apps are expected to return a playable media item as the first 145 * child. If there are no children or it isn't playable it will be ignored. 146 */ 147 @Override 148 public void onConnected() { 149 Log.d(TAG, "Service connected for " + mComponentName); 150 if (mMediaBrowser != null && mMediaBrowser.isConnected()) { 151 String root = mMediaBrowser.getRoot(); 152 if (!TextUtils.isEmpty(root)) { 153 if (mCallback != null) { 154 mCallback.onConnected(); 155 } 156 if (mMediaBrowser != null) { 157 mMediaBrowser.subscribe(root, mSubscriptionCallback); 158 } 159 return; 160 } 161 } 162 if (mCallback != null) { 163 mCallback.onError(); 164 } 165 disconnect(); 166 } 167 168 /** 169 * Invoked when the client is disconnected from the media browser. 170 */ 171 @Override 172 public void onConnectionSuspended() { 173 Log.d(TAG, "Connection suspended for " + mComponentName); 174 if (mCallback != null) { 175 mCallback.onError(); 176 } 177 disconnect(); 178 } 179 180 /** 181 * Invoked when the connection to the media browser failed. 182 */ 183 @Override 184 public void onConnectionFailed() { 185 Log.d(TAG, "Connection failed for " + mComponentName); 186 if (mCallback != null) { 187 mCallback.onError(); 188 } 189 disconnect(); 190 } 191 }; 192 193 /** 194 * Disconnect the media browser. This should be done after callbacks have completed to 195 * disconnect from the media browser service. 196 */ disconnect()197 protected void disconnect() { 198 if (mMediaBrowser != null) { 199 mMediaBrowser.disconnect(); 200 } 201 mMediaBrowser = null; 202 } 203 204 /** 205 * Connects to the MediaBrowserService and starts playback. 206 * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called 207 * depending on whether it was successful. 208 * If the connection is successful, the listener should call ResumeMediaBrowser#disconnect after 209 * getting a media update from the app 210 */ restart()211 public void restart() { 212 disconnect(); 213 Bundle rootHints = new Bundle(); 214 rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); 215 mMediaBrowser = mBrowserFactory.create(mComponentName, 216 new MediaBrowser.ConnectionCallback() { 217 @Override 218 public void onConnected() { 219 Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected()); 220 if (mMediaBrowser == null || !mMediaBrowser.isConnected()) { 221 if (mCallback != null) { 222 mCallback.onError(); 223 } 224 disconnect(); 225 return; 226 } 227 MediaSession.Token token = mMediaBrowser.getSessionToken(); 228 MediaController controller = createMediaController(token); 229 controller.getTransportControls(); 230 controller.getTransportControls().prepare(); 231 controller.getTransportControls().play(); 232 if (mCallback != null) { 233 mCallback.onConnected(); 234 } 235 // listener should disconnect after media player update 236 } 237 238 @Override 239 public void onConnectionFailed() { 240 if (mCallback != null) { 241 mCallback.onError(); 242 } 243 disconnect(); 244 } 245 246 @Override 247 public void onConnectionSuspended() { 248 if (mCallback != null) { 249 mCallback.onError(); 250 } 251 disconnect(); 252 } 253 }, rootHints); 254 mMediaBrowser.connect(); 255 } 256 257 @VisibleForTesting createMediaController(MediaSession.Token token)258 protected MediaController createMediaController(MediaSession.Token token) { 259 return new MediaController(mContext, token); 260 } 261 262 /** 263 * Get the media session token 264 * @return the token, or null if the MediaBrowser is null or disconnected 265 */ getToken()266 public MediaSession.Token getToken() { 267 if (mMediaBrowser == null || !mMediaBrowser.isConnected()) { 268 return null; 269 } 270 return mMediaBrowser.getSessionToken(); 271 } 272 273 /** 274 * Get an intent to launch the app associated with this browser service 275 * @return 276 */ getAppIntent()277 public PendingIntent getAppIntent() { 278 PackageManager pm = mContext.getPackageManager(); 279 Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName()); 280 return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED); 281 } 282 283 /** 284 * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser. 285 * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is 286 * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more 287 * detailed logging if the service has issues. If it cannot connect, or cannot find valid media, 288 * then ResumeMediaBrowser.Callback#onError will be called. 289 * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. 290 */ testConnection()291 public void testConnection() { 292 disconnect(); 293 Bundle rootHints = new Bundle(); 294 rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); 295 mMediaBrowser = mBrowserFactory.create( 296 mComponentName, 297 mConnectionCallback, 298 rootHints); 299 mMediaBrowser.connect(); 300 } 301 302 /** 303 * Interface to handle results from ResumeMediaBrowser 304 */ 305 public static class Callback { 306 /** 307 * Called when the browser has successfully connected to the service 308 */ onConnected()309 public void onConnected() { 310 } 311 312 /** 313 * Called when the browser encountered an error connecting to the service 314 */ onError()315 public void onError() { 316 } 317 318 /** 319 * Called when the browser finds a suitable track to add to the media carousel 320 * @param track media info for the item 321 * @param component component of the MediaBrowserService which returned this 322 * @param browser reference to the browser 323 */ addTrack(MediaDescription track, ComponentName component, ResumeMediaBrowser browser)324 public void addTrack(MediaDescription track, ComponentName component, 325 ResumeMediaBrowser browser) { 326 } 327 } 328 } 329