• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.controls.resume;
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 final MediaBrowserFactory mBrowserFactory;
53     private final ResumeMediaBrowserLogger mLogger;
54     private final ComponentName mComponentName;
55     private final MediaController.Callback mMediaControllerCallback = new SessionDestroyCallback();
56 
57     private MediaBrowser mMediaBrowser;
58     @Nullable private MediaController mMediaController;
59 
60     /**
61      * Initialize a new media browser
62      * @param context the context
63      * @param callback used to report media items found
64      * @param componentName Component name of the MediaBrowserService this browser will connect to
65      */
ResumeMediaBrowser( Context context, @Nullable Callback callback, ComponentName componentName, MediaBrowserFactory browserFactory, ResumeMediaBrowserLogger logger)66     public ResumeMediaBrowser(
67             Context context,
68             @Nullable Callback callback,
69             ComponentName componentName,
70             MediaBrowserFactory browserFactory,
71             ResumeMediaBrowserLogger logger) {
72         mContext = context;
73         mCallback = callback;
74         mComponentName = componentName;
75         mBrowserFactory = browserFactory;
76         mLogger = logger;
77     }
78 
79     /**
80      * Connects to the MediaBrowserService and looks for valid media. If a media item is returned,
81      * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription.
82      * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be
83      * called when the initial connection is successful, or an error occurs.
84      * Note that it is possible for the service to connect but for no playable tracks to be found.
85      * ResumeMediaBrowser#disconnect will be called automatically with this function.
86      */
findRecentMedia()87     public void findRecentMedia() {
88         Bundle rootHints = new Bundle();
89         rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
90         MediaBrowser browser = mBrowserFactory.create(
91                 mComponentName,
92                 mConnectionCallback,
93                 rootHints);
94         connectBrowser(browser, "findRecentMedia");
95     }
96 
97     private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
98             new MediaBrowser.SubscriptionCallback() {
99         @Override
100         public void onChildrenLoaded(String parentId,
101                 List<MediaBrowser.MediaItem> children) {
102             if (children.size() == 0) {
103                 Log.d(TAG, "No children found for " + mComponentName);
104                 if (mCallback != null) {
105                     mCallback.onError();
106                 }
107             } else {
108                 // We ask apps to return a playable item as the first child when sending
109                 // a request with EXTRA_RECENT; if they don't, no resume controls
110                 MediaBrowser.MediaItem child = children.get(0);
111                 MediaDescription desc = child.getDescription();
112                 if (child.isPlayable() && mMediaBrowser != null) {
113                     if (mCallback != null) {
114                         mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
115                                 ResumeMediaBrowser.this);
116                     }
117                 } else {
118                     Log.d(TAG, "Child found but not playable for " + mComponentName);
119                     if (mCallback != null) {
120                         mCallback.onError();
121                     }
122                 }
123             }
124             disconnect();
125         }
126 
127         @Override
128         public void onError(String parentId) {
129             Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
130             if (mCallback != null) {
131                 mCallback.onError();
132             }
133             disconnect();
134         }
135 
136         @Override
137         public void onError(String parentId, Bundle options) {
138             Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId
139                     + ", options: " + options);
140             if (mCallback != null) {
141                 mCallback.onError();
142             }
143             disconnect();
144         }
145     };
146 
147     private final MediaBrowser.ConnectionCallback mConnectionCallback =
148             new MediaBrowser.ConnectionCallback() {
149         /**
150          * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
151          * For resumption controls, apps are expected to return a playable media item as the first
152          * child. If there are no children or it isn't playable it will be ignored.
153          */
154         @Override
155         public void onConnected() {
156             Log.d(TAG, "Service connected for " + mComponentName);
157             updateMediaController();
158             if (isBrowserConnected()) {
159                 String root = mMediaBrowser.getRoot();
160                 if (!TextUtils.isEmpty(root)) {
161                     if (mCallback != null) {
162                         mCallback.onConnected();
163                     }
164                     if (mMediaBrowser != null) {
165                         mMediaBrowser.subscribe(root, mSubscriptionCallback);
166                     }
167                     return;
168                 }
169             }
170             if (mCallback != null) {
171                 mCallback.onError();
172             }
173             disconnect();
174         }
175 
176         /**
177          * Invoked when the client is disconnected from the media browser.
178          */
179         @Override
180         public void onConnectionSuspended() {
181             Log.d(TAG, "Connection suspended for " + mComponentName);
182             if (mCallback != null) {
183                 mCallback.onError();
184             }
185             disconnect();
186         }
187 
188         /**
189          * Invoked when the connection to the media browser failed.
190          */
191         @Override
192         public void onConnectionFailed() {
193             Log.d(TAG, "Connection failed for " + mComponentName);
194             if (mCallback != null) {
195                 mCallback.onError();
196             }
197             disconnect();
198         }
199     };
200 
201     /**
202      * Connect using a new media browser. Disconnects the existing browser first, if it exists.
203      * @param browser media browser to connect
204      * @param reason Reason to log for connection
205      */
connectBrowser(MediaBrowser browser, String reason)206     private void connectBrowser(MediaBrowser browser, String reason) {
207         mLogger.logConnection(mComponentName, reason);
208         disconnect();
209         mMediaBrowser = browser;
210         if (browser != null) {
211             browser.connect();
212         }
213         updateMediaController();
214     }
215 
216     /**
217      * Disconnect the media browser. This should be done after callbacks have completed to
218      * disconnect from the media browser service.
219      */
disconnect()220     protected void disconnect() {
221         if (mMediaBrowser != null) {
222             mLogger.logDisconnect(mComponentName);
223             mMediaBrowser.disconnect();
224         }
225         mMediaBrowser = null;
226         updateMediaController();
227     }
228 
229     /**
230      * Connects to the MediaBrowserService and starts playback.
231      * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
232      * depending on whether it was successful.
233      * If the connection is successful, the listener should call ResumeMediaBrowser#disconnect after
234      * getting a media update from the app
235      */
restart()236     public void restart() {
237         Bundle rootHints = new Bundle();
238         rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
239         MediaBrowser browser = mBrowserFactory.create(mComponentName,
240                 new MediaBrowser.ConnectionCallback() {
241                     @Override
242                     public void onConnected() {
243                         Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected());
244                         updateMediaController();
245                         if (!isBrowserConnected()) {
246                             if (mCallback != null) {
247                                 mCallback.onError();
248                             }
249                             disconnect();
250                             return;
251                         }
252                         MediaSession.Token token = mMediaBrowser.getSessionToken();
253                         MediaController controller = createMediaController(token);
254                         controller.getTransportControls();
255                         controller.getTransportControls().prepare();
256                         controller.getTransportControls().play();
257                         if (mCallback != null) {
258                             mCallback.onConnected();
259                         }
260                         // listener should disconnect after media player update
261                     }
262 
263                     @Override
264                     public void onConnectionFailed() {
265                         if (mCallback != null) {
266                             mCallback.onError();
267                         }
268                         disconnect();
269                     }
270 
271                     @Override
272                     public void onConnectionSuspended() {
273                         if (mCallback != null) {
274                             mCallback.onError();
275                         }
276                         disconnect();
277                     }
278                 }, rootHints);
279         connectBrowser(browser, "restart");
280     }
281 
282     @VisibleForTesting
createMediaController(MediaSession.Token token)283     protected MediaController createMediaController(MediaSession.Token token) {
284         return new MediaController(mContext, token);
285     }
286 
287     /**
288      * Get the media session token
289      * @return the token, or null if the MediaBrowser is null or disconnected
290      */
getToken()291     public MediaSession.Token getToken() {
292         if (!isBrowserConnected()) {
293             return null;
294         }
295         return mMediaBrowser.getSessionToken();
296     }
297 
298     /**
299      * Get an intent to launch the app associated with this browser service
300      * @return
301      */
getAppIntent()302     public PendingIntent getAppIntent() {
303         PackageManager pm = mContext.getPackageManager();
304         Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
305         return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE);
306     }
307 
308     /**
309      * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
310      * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is
311      * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more
312      * detailed logging if the service has issues. If it cannot connect, or cannot find valid media,
313      * then ResumeMediaBrowser.Callback#onError will be called.
314      * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
315      */
testConnection()316     public void testConnection() {
317         Bundle rootHints = new Bundle();
318         rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
319         MediaBrowser browser = mBrowserFactory.create(
320                 mComponentName,
321                 mConnectionCallback,
322                 rootHints);
323         connectBrowser(browser, "testConnection");
324     }
325 
326     /** Updates mMediaController based on our current browser values. */
updateMediaController()327     private void updateMediaController() {
328         MediaSession.Token controllerToken =
329                 mMediaController != null ? mMediaController.getSessionToken() : null;
330         MediaSession.Token currentToken = getToken();
331         boolean areEqual = (controllerToken == null && currentToken == null)
332                 || (controllerToken != null && controllerToken.equals(currentToken));
333         if (areEqual) {
334             return;
335         }
336 
337         // Whenever the token changes, un-register the callback on the old controller (if we have
338         // one) and create a new controller with the callback attached.
339         if (mMediaController != null) {
340             mMediaController.unregisterCallback(mMediaControllerCallback);
341         }
342         if (currentToken != null) {
343             mMediaController = createMediaController(currentToken);
344             mMediaController.registerCallback(mMediaControllerCallback);
345         } else {
346             mMediaController = null;
347         }
348     }
349 
isBrowserConnected()350     private boolean isBrowserConnected() {
351         return mMediaBrowser != null && mMediaBrowser.isConnected();
352     }
353 
354     /**
355      * Interface to handle results from ResumeMediaBrowser
356      */
357     public static class Callback {
358         /**
359          * Called when the browser has successfully connected to the service
360          */
onConnected()361         public void onConnected() {
362         }
363 
364         /**
365          * Called when the browser encountered an error connecting to the service
366          */
onError()367         public void onError() {
368         }
369 
370         /**
371          * Called when the browser finds a suitable track to add to the media carousel
372          * @param track media info for the item
373          * @param component component of the MediaBrowserService which returned this
374          * @param browser reference to the browser
375          */
addTrack(MediaDescription track, ComponentName component, ResumeMediaBrowser browser)376         public void addTrack(MediaDescription track, ComponentName component,
377                 ResumeMediaBrowser browser) {
378         }
379     }
380 
381     private class SessionDestroyCallback extends MediaController.Callback {
382         @Override
onSessionDestroyed()383         public void onSessionDestroyed() {
384             mLogger.logSessionDestroyed(isBrowserConnected(), mComponentName);
385             disconnect();
386         }
387     }
388 }
389