1 /*
2  * Copyright 2018 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 android.media;
18 
19 import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
20 import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
21 import static android.media.MediaConstants.KEY_PACKAGE_NAME;
22 import static android.media.MediaConstants.KEY_PID;
23 import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
24 import static android.media.MediaConstants.KEY_SESSION2LINK;
25 import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
26 import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
27 import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
28 import static android.media.Session2Token.TYPE_SESSION;
29 
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.app.PendingIntent;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.media.session.MediaSessionManager;
36 import android.media.session.MediaSessionManager.RemoteUserInfo;
37 import android.os.BadParcelableException;
38 import android.os.Bundle;
39 import android.os.Handler;
40 import android.os.Parcel;
41 import android.os.Process;
42 import android.os.ResultReceiver;
43 import android.util.ArrayMap;
44 import android.util.ArraySet;
45 import android.util.Log;
46 
47 import com.android.modules.utils.build.SdkLevel;
48 
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Objects;
54 import java.util.concurrent.Executor;
55 
56 /**
57  * This API is not generally intended for third party application developers.
58  * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
59  * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
60  * Library</a> for consistent behavior across all devices.
61  * <p>
62  * Allows a media app to expose its transport controls and playback information in a process to
63  * other processes including the Android framework and other apps.
64  */
65 public class MediaSession2 implements AutoCloseable {
66     static final String TAG = "MediaSession2";
67     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
68 
69     // Note: This checks the uniqueness of a session ID only in a single process.
70     // When the framework becomes able to check the uniqueness, this logic should be removed.
71     //@GuardedBy("MediaSession.class")
72     private static final List<String> SESSION_ID_LIST = new ArrayList<>();
73 
74     @SuppressWarnings("WeakerAccess") /* synthetic access */
75     final Object mLock = new Object();
76     //@GuardedBy("mLock")
77     @SuppressWarnings("WeakerAccess") /* synthetic access */
78     final Map<Controller2Link, ControllerInfo> mConnectedControllers = new HashMap<>();
79 
80     @SuppressWarnings("WeakerAccess") /* synthetic access */
81     final Context mContext;
82     @SuppressWarnings("WeakerAccess") /* synthetic access */
83     final Executor mCallbackExecutor;
84     @SuppressWarnings("WeakerAccess") /* synthetic access */
85     final SessionCallback mCallback;
86     @SuppressWarnings("WeakerAccess") /* synthetic access */
87     final Session2Link mSessionStub;
88 
89     private final String mSessionId;
90     private final PendingIntent mSessionActivity;
91     private final Session2Token mSessionToken;
92     private final MediaSessionManager mMediaSessionManager;
93     private final MediaCommunicationManager mCommunicationManager;
94     private final Handler mResultHandler;
95 
96     //@GuardedBy("mLock")
97     private boolean mClosed;
98     //@GuardedBy("mLock")
99     private boolean mPlaybackActive;
100     //@GuardedBy("mLock")
101     private ForegroundServiceEventCallback mForegroundServiceEventCallback;
102 
MediaSession2(@onNull Context context, @NonNull String id, PendingIntent sessionActivity, @NonNull Executor callbackExecutor, @NonNull SessionCallback callback, @NonNull Bundle tokenExtras)103     MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity,
104             @NonNull Executor callbackExecutor, @NonNull SessionCallback callback,
105             @NonNull Bundle tokenExtras) {
106         synchronized (MediaSession2.class) {
107             if (SESSION_ID_LIST.contains(id)) {
108                 throw new IllegalStateException("Session ID must be unique. ID=" + id);
109             }
110             SESSION_ID_LIST.add(id);
111         }
112 
113         mContext = context;
114         mSessionId = id;
115         mSessionActivity = sessionActivity;
116         mCallbackExecutor = callbackExecutor;
117         mCallback = callback;
118         mSessionStub = new Session2Link(this);
119         mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
120                 mSessionStub, tokenExtras);
121         if (SdkLevel.isAtLeastS()) {
122             mCommunicationManager = mContext.getSystemService(MediaCommunicationManager.class);
123             mMediaSessionManager = null;
124         } else {
125             mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
126             mCommunicationManager = null;
127         }
128         // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
129         mResultHandler = new Handler(context.getMainLooper());
130         mClosed = false;
131     }
132 
133     @Override
close()134     public void close() {
135         try {
136             List<ControllerInfo> controllerInfos;
137             ForegroundServiceEventCallback callback;
138             synchronized (mLock) {
139                 if (mClosed) {
140                     return;
141                 }
142                 mClosed = true;
143                 controllerInfos = getConnectedControllers();
144                 mConnectedControllers.clear();
145                 callback = mForegroundServiceEventCallback;
146                 mForegroundServiceEventCallback = null;
147             }
148             synchronized (MediaSession2.class) {
149                 SESSION_ID_LIST.remove(mSessionId);
150             }
151             if (callback != null) {
152                 callback.onSessionClosed(this);
153             }
154             for (ControllerInfo info : controllerInfos) {
155                 info.notifyDisconnected();
156             }
157         } catch (Exception e) {
158             // Should not be here.
159         }
160     }
161 
162     /**
163      * Returns the session ID
164      */
165     @NonNull
getId()166     public String getId() {
167         return mSessionId;
168     }
169 
170     /**
171      * Returns the {@link Session2Token} for creating {@link MediaController2}.
172      */
173     @NonNull
getToken()174     public Session2Token getToken() {
175         return mSessionToken;
176     }
177 
178     /**
179      * Broadcasts a session command to all the connected controllers
180      * <p>
181      * @param command the session command
182      * @param args optional arguments
183      */
broadcastSessionCommand(@onNull Session2Command command, @Nullable Bundle args)184     public void broadcastSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
185         if (command == null) {
186             throw new IllegalArgumentException("command shouldn't be null");
187         }
188         List<ControllerInfo> controllerInfos = getConnectedControllers();
189         for (ControllerInfo controller : controllerInfos) {
190             controller.sendSessionCommand(command, args, null);
191         }
192     }
193 
194     /**
195      * Sends a session command to a specific controller
196      * <p>
197      * @param controller the controller to get the session command
198      * @param command the session command
199      * @param args optional arguments
200      * @return a token which will be sent together in {@link SessionCallback#onCommandResult}
201      *     when its result is received.
202      */
203     @NonNull
sendSessionCommand(@onNull ControllerInfo controller, @NonNull Session2Command command, @Nullable Bundle args)204     public Object sendSessionCommand(@NonNull ControllerInfo controller,
205             @NonNull Session2Command command, @Nullable Bundle args) {
206         if (controller == null) {
207             throw new IllegalArgumentException("controller shouldn't be null");
208         }
209         if (command == null) {
210             throw new IllegalArgumentException("command shouldn't be null");
211         }
212         ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
213             protected void onReceiveResult(int resultCode, Bundle resultData) {
214                 controller.receiveCommandResult(this);
215                 mCallbackExecutor.execute(() -> {
216                     mCallback.onCommandResult(MediaSession2.this, controller, this,
217                             command, new Session2Command.Result(resultCode, resultData));
218                 });
219             }
220         };
221         controller.sendSessionCommand(command, args, resultReceiver);
222         return resultReceiver;
223     }
224 
225     /**
226      * Cancels the session command previously sent.
227      *
228      * @param controller the controller to get the session command
229      * @param token the token which is returned from {@link #sendSessionCommand}.
230      */
cancelSessionCommand(@onNull ControllerInfo controller, @NonNull Object token)231     public void cancelSessionCommand(@NonNull ControllerInfo controller, @NonNull Object token) {
232         if (controller == null) {
233             throw new IllegalArgumentException("controller shouldn't be null");
234         }
235         if (token == null) {
236             throw new IllegalArgumentException("token shouldn't be null");
237         }
238         controller.cancelSessionCommand(token);
239     }
240 
241     /**
242      * Sets whether the playback is active (i.e. playing something)
243      *
244      * @param playbackActive {@code true} if the playback active, {@code false} otherwise.
245      **/
setPlaybackActive(boolean playbackActive)246     public void setPlaybackActive(boolean playbackActive) {
247         final ForegroundServiceEventCallback serviceCallback;
248         synchronized (mLock) {
249             if (mPlaybackActive == playbackActive) {
250                 return;
251             }
252             mPlaybackActive = playbackActive;
253             serviceCallback = mForegroundServiceEventCallback;
254         }
255         if (serviceCallback != null) {
256             serviceCallback.onPlaybackActiveChanged(this, playbackActive);
257         }
258         List<ControllerInfo> controllerInfos = getConnectedControllers();
259         for (ControllerInfo controller : controllerInfos) {
260             controller.notifyPlaybackActiveChanged(playbackActive);
261         }
262     }
263 
264     /**
265      * Returns whether the playback is active (i.e. playing something)
266      *
267      * @return {@code true} if the playback active, {@code false} otherwise.
268      */
isPlaybackActive()269     public boolean isPlaybackActive() {
270         synchronized (mLock) {
271             return mPlaybackActive;
272         }
273     }
274 
275     /**
276      * Gets the list of the connected controllers
277      *
278      * @return list of the connected controllers.
279      */
280     @NonNull
getConnectedControllers()281     public List<ControllerInfo> getConnectedControllers() {
282         List<ControllerInfo> controllers = new ArrayList<>();
283         synchronized (mLock) {
284             controllers.addAll(mConnectedControllers.values());
285         }
286         return controllers;
287     }
288 
289     /**
290      * Returns whether the given bundle includes non-framework Parcelables.
291      */
hasCustomParcelable(@ullable Bundle bundle)292     static boolean hasCustomParcelable(@Nullable Bundle bundle) {
293         if (bundle == null) {
294             return false;
295         }
296 
297         // Try writing the bundle to parcel, and read it with framework classloader.
298         Parcel parcel = null;
299         try {
300             parcel = Parcel.obtain();
301             parcel.writeBundle(bundle);
302             parcel.setDataPosition(0);
303             Bundle out = parcel.readBundle(null);
304 
305             for (String key : out.keySet()) {
306                 out.get(key);
307             }
308         } catch (BadParcelableException e) {
309             Log.d(TAG, "Custom parcelable in bundle.", e);
310             return true;
311         } finally {
312             if (parcel != null) {
313                 parcel.recycle();
314             }
315         }
316         return false;
317     }
318 
isClosed()319     boolean isClosed() {
320         synchronized (mLock) {
321             return mClosed;
322         }
323     }
324 
getCallback()325     SessionCallback getCallback() {
326         return mCallback;
327     }
328 
isTrustedForMediaControl(RemoteUserInfo remoteUserInfo)329     boolean isTrustedForMediaControl(RemoteUserInfo remoteUserInfo) {
330         if (SdkLevel.isAtLeastS()) {
331             return mCommunicationManager.isTrustedForMediaControl(remoteUserInfo);
332         } else {
333             return mMediaSessionManager.isTrustedForMediaControl(remoteUserInfo);
334         }
335     }
336 
setForegroundServiceEventCallback(ForegroundServiceEventCallback callback)337     void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
338         synchronized (mLock) {
339             if (mForegroundServiceEventCallback == callback) {
340                 return;
341             }
342             if (mForegroundServiceEventCallback != null && callback != null) {
343                 throw new IllegalStateException("A session cannot be added to multiple services");
344             }
345             mForegroundServiceEventCallback = callback;
346         }
347     }
348 
349     // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, Bundle connectionRequest)350     void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
351             Bundle connectionRequest) {
352         if (callingPid == 0) {
353             // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from
354             // the remote process. If it's the case, use PID from the connectionRequest.
355             callingPid = connectionRequest.getInt(KEY_PID);
356         }
357         String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
358 
359         RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
360 
361         Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
362         if (connectionHints == null) {
363             Log.w(TAG, "connectionHints shouldn't be null.");
364             connectionHints = Bundle.EMPTY;
365         } else if (hasCustomParcelable(connectionHints)) {
366             Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
367             connectionHints = Bundle.EMPTY;
368         }
369 
370         final ControllerInfo controllerInfo = new ControllerInfo(
371                 remoteUserInfo,
372                 isTrustedForMediaControl(remoteUserInfo),
373                 controller,
374                 connectionHints);
375         mCallbackExecutor.execute(() -> {
376             boolean connected = false;
377             try {
378                 if (isClosed()) {
379                     return;
380                 }
381                 controllerInfo.mAllowedCommands =
382                         mCallback.onConnect(MediaSession2.this, controllerInfo);
383                 // Don't reject connection for the request from trusted app.
384                 // Otherwise server will fail to retrieve session's information to dispatch
385                 // media keys to.
386                 if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) {
387                     return;
388                 }
389                 if (controllerInfo.mAllowedCommands == null) {
390                     // For trusted apps, send non-null allowed commands to keep
391                     // connection.
392                     controllerInfo.mAllowedCommands =
393                             new Session2CommandGroup.Builder().build();
394                 }
395                 if (DEBUG) {
396                     Log.d(TAG, "Accepting connection: " + controllerInfo);
397                 }
398                 // If connection is accepted, notify the current state to the controller.
399                 // It's needed because we cannot call synchronous calls between
400                 // session/controller.
401                 Bundle connectionResult = new Bundle();
402                 connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
403                 connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
404                         controllerInfo.mAllowedCommands);
405                 connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive());
406                 connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras());
407 
408                 // Double check if session is still there, because close() can be called in
409                 // another thread.
410                 if (isClosed()) {
411                     return;
412                 }
413                 controllerInfo.notifyConnected(connectionResult);
414                 synchronized (mLock) {
415                     if (mConnectedControllers.containsKey(controller)) {
416                         Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
417                                 + " request multiple times");
418                     }
419                     mConnectedControllers.put(controller, controllerInfo);
420                 }
421                 mCallback.onPostConnect(MediaSession2.this, controllerInfo);
422                 connected = true;
423             } finally {
424                 if (!connected || isClosed()) {
425                     if (DEBUG) {
426                         Log.d(TAG, "Rejecting connection or notifying that session is closed"
427                                 + ", controllerInfo=" + controllerInfo);
428                     }
429                     synchronized (mLock) {
430                         mConnectedControllers.remove(controller);
431                     }
432                     controllerInfo.notifyDisconnected();
433                 }
434             }
435         });
436     }
437 
438     // Called by Session2Link.onDisconnect
onDisconnect(@onNull final Controller2Link controller, int seq)439     void onDisconnect(@NonNull final Controller2Link controller, int seq) {
440         final ControllerInfo controllerInfo;
441         synchronized (mLock) {
442             controllerInfo = mConnectedControllers.remove(controller);
443         }
444         if (controllerInfo == null) {
445             return;
446         }
447         mCallbackExecutor.execute(() -> {
448             mCallback.onDisconnected(MediaSession2.this, controllerInfo);
449         });
450     }
451 
452     // Called by Session2Link.onSessionCommand
onSessionCommand(@onNull final Controller2Link controller, final int seq, final Session2Command command, final Bundle args, @Nullable ResultReceiver resultReceiver)453     void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
454             final Session2Command command, final Bundle args,
455             @Nullable ResultReceiver resultReceiver) {
456         if (controller == null) {
457             return;
458         }
459         final ControllerInfo controllerInfo;
460         synchronized (mLock) {
461             controllerInfo = mConnectedControllers.get(controller);
462         }
463         if (controllerInfo == null) {
464             return;
465         }
466 
467         // TODO: check allowed commands.
468         synchronized (mLock) {
469             controllerInfo.addRequestedCommandSeqNumber(seq);
470         }
471         mCallbackExecutor.execute(() -> {
472             if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) {
473                 if (resultReceiver != null) {
474                     resultReceiver.send(RESULT_INFO_SKIPPED, null);
475                 }
476                 return;
477             }
478             Session2Command.Result result = mCallback.onSessionCommand(
479                     MediaSession2.this, controllerInfo, command, args);
480             if (resultReceiver != null) {
481                 if (result == null) {
482                     resultReceiver.send(RESULT_INFO_SKIPPED, null);
483                 } else {
484                     resultReceiver.send(result.getResultCode(), result.getResultData());
485                 }
486             }
487         });
488     }
489 
490     // Called by Session2Link.onCancelCommand
onCancelCommand(@onNull final Controller2Link controller, final int seq)491     void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
492         final ControllerInfo controllerInfo;
493         synchronized (mLock) {
494             controllerInfo = mConnectedControllers.get(controller);
495         }
496         if (controllerInfo == null) {
497             return;
498         }
499         controllerInfo.removeRequestedCommandSeqNumber(seq);
500     }
501 
502     /**
503      * This API is not generally intended for third party application developers.
504      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
505      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
506      * Library</a> for consistent behavior across all devices.
507      * <p>
508      * Builder for {@link MediaSession2}.
509      * <p>
510      * Any incoming event from the {@link MediaController2} will be handled on the callback
511      * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
512      */
513     public static final class Builder {
514         private Context mContext;
515         private String mId;
516         private PendingIntent mSessionActivity;
517         private Executor mCallbackExecutor;
518         private SessionCallback mCallback;
519         private Bundle mExtras;
520 
521         /**
522          * Creates a builder for {@link MediaSession2}.
523          *
524          * @param context Context
525          * @throws IllegalArgumentException if context is {@code null}.
526          */
Builder(@onNull Context context)527         public Builder(@NonNull Context context) {
528             if (context == null) {
529                 throw new IllegalArgumentException("context shouldn't be null");
530             }
531             mContext = context;
532         }
533 
534         /**
535          * Set an intent for launching UI for this Session. This can be used as a
536          * quick link to an ongoing media screen. The intent should be for an
537          * activity that may be started using {@link Context#startActivity(Intent)}.
538          *
539          * @param pi The intent to launch to show UI for this session.
540          * @return The Builder to allow chaining
541          */
542         @NonNull
setSessionActivity(@ullable PendingIntent pi)543         public Builder setSessionActivity(@Nullable PendingIntent pi) {
544             mSessionActivity = pi;
545             return this;
546         }
547 
548         /**
549          * Set ID of the session. If it's not set, an empty string will be used to create a session.
550          * <p>
551          * Use this if and only if your app supports multiple playback at the same time and also
552          * wants to provide external apps to have finer controls of them.
553          *
554          * @param id id of the session. Must be unique per package.
555          * @throws IllegalArgumentException if id is {@code null}.
556          * @return The Builder to allow chaining
557          */
558         @NonNull
setId(@onNull String id)559         public Builder setId(@NonNull String id) {
560             if (id == null) {
561                 throw new IllegalArgumentException("id shouldn't be null");
562             }
563             mId = id;
564             return this;
565         }
566 
567         /**
568          * Set callback for the session and its executor.
569          *
570          * @param executor callback executor
571          * @param callback session callback.
572          * @return The Builder to allow chaining
573          */
574         @NonNull
setSessionCallback(@onNull Executor executor, @NonNull SessionCallback callback)575         public Builder setSessionCallback(@NonNull Executor executor,
576                 @NonNull SessionCallback callback) {
577             mCallbackExecutor = executor;
578             mCallback = callback;
579             return this;
580         }
581 
582         /**
583          * Set extras for the session token. If null or not set, {@link Session2Token#getExtras()}
584          * will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown
585          * if the bundle contains any non-framework Parcelable objects.
586          *
587          * @return The Builder to allow chaining
588          * @see Session2Token#getExtras()
589          */
590         @NonNull
setExtras(@onNull Bundle extras)591         public Builder setExtras(@NonNull Bundle extras) {
592             if (extras == null) {
593                 throw new NullPointerException("extras shouldn't be null");
594             }
595             if (hasCustomParcelable(extras)) {
596                 throw new IllegalArgumentException(
597                         "extras shouldn't contain any custom parcelables");
598             }
599             mExtras = new Bundle(extras);
600             return this;
601         }
602 
603         /**
604          * Build {@link MediaSession2}.
605          *
606          * @return a new session
607          * @throws IllegalStateException if the session with the same id is already exists for the
608          *      package.
609          */
610         @NonNull
build()611         public MediaSession2 build() {
612             if (mCallbackExecutor == null) {
613                 mCallbackExecutor = mContext.getMainExecutor();
614             }
615             if (mCallback == null) {
616                 mCallback = new SessionCallback() {};
617             }
618             if (mId == null) {
619                 mId = "";
620             }
621             if (mExtras == null) {
622                 mExtras = Bundle.EMPTY;
623             }
624             MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity,
625                     mCallbackExecutor, mCallback, mExtras);
626 
627             // Notify framework about the newly create session after the constructor is finished.
628             // Otherwise, framework may access the session before the initialization is finished.
629             try {
630                 if (SdkLevel.isAtLeastS()) {
631                     MediaCommunicationManager manager =
632                             mContext.getSystemService(MediaCommunicationManager.class);
633                     manager.notifySession2Created(session2.getToken());
634                 } else {
635                     MediaSessionManager manager =
636                             mContext.getSystemService(MediaSessionManager.class);
637                     manager.notifySession2Created(session2.getToken());
638                 }
639             } catch (Exception e) {
640                 session2.close();
641                 throw e;
642             }
643 
644             return session2;
645         }
646     }
647 
648     /**
649      * This API is not generally intended for third party application developers.
650      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
651      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
652      * Library</a> for consistent behavior across all devices.
653      * <p>
654      * Information of a controller.
655      */
656     public static final class ControllerInfo {
657         private final RemoteUserInfo mRemoteUserInfo;
658         private final boolean mIsTrusted;
659         private final Controller2Link mControllerBinder;
660         private final Bundle mConnectionHints;
661         private final Object mLock = new Object();
662         //@GuardedBy("mLock")
663         private int mNextSeqNumber;
664         //@GuardedBy("mLock")
665         private ArrayMap<ResultReceiver, Integer> mPendingCommands;
666         //@GuardedBy("mLock")
667         private ArraySet<Integer> mRequestedCommandSeqNumbers;
668 
669         @SuppressWarnings("WeakerAccess") /* synthetic access */
670         Session2CommandGroup mAllowedCommands;
671 
672         /**
673          * @param remoteUserInfo remote user info
674          * @param trusted {@code true} if trusted, {@code false} otherwise
675          * @param controllerBinder Controller2Link for the connected controller.
676          * @param connectionHints a session-specific argument sent from the controller for the
677          *                        connection. The contents of this bundle may affect the
678          *                        connection result.
679          */
ControllerInfo(@onNull RemoteUserInfo remoteUserInfo, boolean trusted, @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints)680         ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
681                 @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) {
682             mRemoteUserInfo = remoteUserInfo;
683             mIsTrusted = trusted;
684             mControllerBinder = controllerBinder;
685             mConnectionHints = connectionHints;
686             mPendingCommands = new ArrayMap<>();
687             mRequestedCommandSeqNumbers = new ArraySet<>();
688         }
689 
690         /**
691          * @return remote user info of the controller.
692          */
693         @NonNull
getRemoteUserInfo()694         public RemoteUserInfo getRemoteUserInfo() {
695             return mRemoteUserInfo;
696         }
697 
698         /**
699          * @return package name of the controller.
700          */
701         @NonNull
getPackageName()702         public String getPackageName() {
703             return mRemoteUserInfo.getPackageName();
704         }
705 
706         /**
707          * @return uid of the controller. Can be a negative value if the uid cannot be obtained.
708          */
getUid()709         public int getUid() {
710             return mRemoteUserInfo.getUid();
711         }
712 
713         /**
714          * @return connection hints sent from controller.
715          */
716         @NonNull
getConnectionHints()717         public Bundle getConnectionHints() {
718             return new Bundle(mConnectionHints);
719         }
720 
721         /**
722          * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
723          * has a enabled notification listener so can be trusted to accept connection and incoming
724          * command request.
725          *
726          * @return {@code true} if the controller is trusted.
727          * @hide
728          */
isTrusted()729         public boolean isTrusted() {
730             return mIsTrusted;
731         }
732 
733         @Override
hashCode()734         public int hashCode() {
735             return Objects.hash(mControllerBinder, mRemoteUserInfo);
736         }
737 
738         @Override
equals(@ullable Object obj)739         public boolean equals(@Nullable Object obj) {
740             if (!(obj instanceof ControllerInfo)) return false;
741             if (this == obj) return true;
742 
743             ControllerInfo other = (ControllerInfo) obj;
744             if (mControllerBinder != null || other.mControllerBinder != null) {
745                 return Objects.equals(mControllerBinder, other.mControllerBinder);
746             }
747             return mRemoteUserInfo.equals(other.mRemoteUserInfo);
748         }
749 
750         @Override
751         @NonNull
toString()752         public String toString() {
753             return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
754                     + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})";
755         }
756 
notifyConnected(Bundle connectionResult)757         void notifyConnected(Bundle connectionResult) {
758             if (mControllerBinder == null) return;
759 
760             try {
761                 mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult);
762             } catch (RuntimeException e) {
763                 // Controller may be died prematurely.
764             }
765         }
766 
notifyDisconnected()767         void notifyDisconnected() {
768             if (mControllerBinder == null) return;
769 
770             try {
771                 mControllerBinder.notifyDisconnected(getNextSeqNumber());
772             } catch (RuntimeException e) {
773                 // Controller may be died prematurely.
774             }
775         }
776 
notifyPlaybackActiveChanged(boolean playbackActive)777         void notifyPlaybackActiveChanged(boolean playbackActive) {
778             if (mControllerBinder == null) return;
779 
780             try {
781                 mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive);
782             } catch (RuntimeException e) {
783                 // Controller may be died prematurely.
784             }
785         }
786 
sendSessionCommand(Session2Command command, Bundle args, ResultReceiver resultReceiver)787         void sendSessionCommand(Session2Command command, Bundle args,
788                 ResultReceiver resultReceiver) {
789             if (mControllerBinder == null) return;
790 
791             try {
792                 int seq = getNextSeqNumber();
793                 synchronized (mLock) {
794                     mPendingCommands.put(resultReceiver, seq);
795                 }
796                 mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver);
797             } catch (RuntimeException e) {
798                 // Controller may be died prematurely.
799                 synchronized (mLock) {
800                     mPendingCommands.remove(resultReceiver);
801                 }
802                 resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
803             }
804         }
805 
cancelSessionCommand(@onNull Object token)806         void cancelSessionCommand(@NonNull Object token) {
807             if (mControllerBinder == null) return;
808             Integer seq;
809             synchronized (mLock) {
810                 seq = mPendingCommands.remove(token);
811             }
812             if (seq != null) {
813                 mControllerBinder.cancelSessionCommand(seq);
814             }
815         }
816 
receiveCommandResult(ResultReceiver resultReceiver)817         void receiveCommandResult(ResultReceiver resultReceiver) {
818             synchronized (mLock) {
819                 mPendingCommands.remove(resultReceiver);
820             }
821         }
822 
addRequestedCommandSeqNumber(int seq)823         void addRequestedCommandSeqNumber(int seq) {
824             synchronized (mLock) {
825                 mRequestedCommandSeqNumbers.add(seq);
826             }
827         }
828 
removeRequestedCommandSeqNumber(int seq)829         boolean removeRequestedCommandSeqNumber(int seq) {
830             synchronized (mLock) {
831                 return mRequestedCommandSeqNumbers.remove(seq);
832             }
833         }
834 
getNextSeqNumber()835         private int getNextSeqNumber() {
836             synchronized (mLock) {
837                 return mNextSeqNumber++;
838             }
839         }
840     }
841 
842     /**
843      * This API is not generally intended for third party application developers.
844      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
845      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
846      * Library</a> for consistent behavior across all devices.
847      * <p>
848      * Callback to be called for all incoming commands from {@link MediaController2}s.
849      */
850     public abstract static class SessionCallback {
851         /**
852          * Called when a controller is created for this session. Return allowed commands for
853          * controller. By default it returns {@code null}.
854          * <p>
855          * You can reject the connection by returning {@code null}. In that case, controller
856          * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
857          * and cannot be used.
858          * <p>
859          * The controller hasn't connected yet in this method, so calls to the controller
860          * (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for
861          * the custom initialization for the controller instead.
862          *
863          * @param session the session for this event
864          * @param controller controller information.
865          * @return allowed commands. Can be {@code null} to reject connection.
866          */
867         @Nullable
onConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)868         public Session2CommandGroup onConnect(@NonNull MediaSession2 session,
869                 @NonNull ControllerInfo controller) {
870             return null;
871         }
872 
873         /**
874          * Called immediately after a controller is connected. This is a convenient method to add
875          * custom initialization between the session and a controller.
876          * <p>
877          * Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't
878          * work in {@link #onConnect} because the controller hasn't connected yet in
879          * {@link #onConnect}.
880          *
881          * @param session the session for this event
882          * @param controller controller information.
883          */
onPostConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)884         public void onPostConnect(@NonNull MediaSession2 session,
885                 @NonNull ControllerInfo controller) {
886         }
887 
888         /**
889          * Called when a controller is disconnected
890          *
891          * @param session the session for this event
892          * @param controller controller information
893          */
onDisconnected(@onNull MediaSession2 session, @NonNull ControllerInfo controller)894         public void onDisconnected(@NonNull MediaSession2 session,
895                 @NonNull ControllerInfo controller) {}
896 
897         /**
898          * Called when a controller sent a session command.
899          *
900          * @param session the session for this event
901          * @param controller controller information
902          * @param command the session command
903          * @param args optional arguments
904          * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
905          *         will be sent to the session.
906          */
907         @Nullable
onSessionCommand(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Session2Command command, @Nullable Bundle args)908         public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session,
909                 @NonNull ControllerInfo controller, @NonNull Session2Command command,
910                 @Nullable Bundle args) {
911             return null;
912         }
913 
914         /**
915          * Called when the command sent to the controller is finished.
916          *
917          * @param session the session for this event
918          * @param controller controller information
919          * @param token the token got from {@link MediaSession2#sendSessionCommand}
920          * @param command the session command
921          * @param result the result of the session command
922          */
onCommandResult(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Object token, @NonNull Session2Command command, @NonNull Session2Command.Result result)923         public void onCommandResult(@NonNull MediaSession2 session,
924                 @NonNull ControllerInfo controller, @NonNull Object token,
925                 @NonNull Session2Command command, @NonNull Session2Command.Result result) {}
926     }
927 
928     abstract static class ForegroundServiceEventCallback {
onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive)929         public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {}
onSessionClosed(MediaSession2 session)930         public void onSessionClosed(MediaSession2 session) {}
931     }
932 }
933