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 whehther 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             // Calling Bundle#size() will trigger Bundle#unparcel().
306             out.size();
307         } catch (BadParcelableException e) {
308             Log.d(TAG, "Custom parcelable in bundle.", e);
309             return true;
310         } finally {
311             if (parcel != null) {
312                 parcel.recycle();
313             }
314         }
315         return false;
316     }
317 
isClosed()318     boolean isClosed() {
319         synchronized (mLock) {
320             return mClosed;
321         }
322     }
323 
getCallback()324     SessionCallback getCallback() {
325         return mCallback;
326     }
327 
isTrustedForMediaControl(RemoteUserInfo remoteUserInfo)328     boolean isTrustedForMediaControl(RemoteUserInfo remoteUserInfo) {
329         if (SdkLevel.isAtLeastS()) {
330             return mCommunicationManager.isTrustedForMediaControl(remoteUserInfo);
331         } else {
332             return mMediaSessionManager.isTrustedForMediaControl(remoteUserInfo);
333         }
334     }
335 
setForegroundServiceEventCallback(ForegroundServiceEventCallback callback)336     void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
337         synchronized (mLock) {
338             if (mForegroundServiceEventCallback == callback) {
339                 return;
340             }
341             if (mForegroundServiceEventCallback != null && callback != null) {
342                 throw new IllegalStateException("A session cannot be added to multiple services");
343             }
344             mForegroundServiceEventCallback = callback;
345         }
346     }
347 
348     // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, Bundle connectionRequest)349     void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
350             Bundle connectionRequest) {
351         if (callingPid == 0) {
352             // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from
353             // the remote process. If it's the case, use PID from the connectionRequest.
354             callingPid = connectionRequest.getInt(KEY_PID);
355         }
356         String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
357 
358         RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
359 
360         Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
361         if (connectionHints == null) {
362             Log.w(TAG, "connectionHints shouldn't be null.");
363             connectionHints = Bundle.EMPTY;
364         } else if (hasCustomParcelable(connectionHints)) {
365             Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
366             connectionHints = Bundle.EMPTY;
367         }
368 
369         final ControllerInfo controllerInfo = new ControllerInfo(
370                 remoteUserInfo,
371                 isTrustedForMediaControl(remoteUserInfo),
372                 controller,
373                 connectionHints);
374         mCallbackExecutor.execute(() -> {
375             boolean connected = false;
376             try {
377                 if (isClosed()) {
378                     return;
379                 }
380                 controllerInfo.mAllowedCommands =
381                         mCallback.onConnect(MediaSession2.this, controllerInfo);
382                 // Don't reject connection for the request from trusted app.
383                 // Otherwise server will fail to retrieve session's information to dispatch
384                 // media keys to.
385                 if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) {
386                     return;
387                 }
388                 if (controllerInfo.mAllowedCommands == null) {
389                     // For trusted apps, send non-null allowed commands to keep
390                     // connection.
391                     controllerInfo.mAllowedCommands =
392                             new Session2CommandGroup.Builder().build();
393                 }
394                 if (DEBUG) {
395                     Log.d(TAG, "Accepting connection: " + controllerInfo);
396                 }
397                 // If connection is accepted, notify the current state to the controller.
398                 // It's needed because we cannot call synchronous calls between
399                 // session/controller.
400                 Bundle connectionResult = new Bundle();
401                 connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
402                 connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
403                         controllerInfo.mAllowedCommands);
404                 connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive());
405                 connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras());
406 
407                 // Double check if session is still there, because close() can be called in
408                 // another thread.
409                 if (isClosed()) {
410                     return;
411                 }
412                 controllerInfo.notifyConnected(connectionResult);
413                 synchronized (mLock) {
414                     if (mConnectedControllers.containsKey(controller)) {
415                         Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
416                                 + " request multiple times");
417                     }
418                     mConnectedControllers.put(controller, controllerInfo);
419                 }
420                 mCallback.onPostConnect(MediaSession2.this, controllerInfo);
421                 connected = true;
422             } finally {
423                 if (!connected || isClosed()) {
424                     if (DEBUG) {
425                         Log.d(TAG, "Rejecting connection or notifying that session is closed"
426                                 + ", controllerInfo=" + controllerInfo);
427                     }
428                     synchronized (mLock) {
429                         mConnectedControllers.remove(controller);
430                     }
431                     controllerInfo.notifyDisconnected();
432                 }
433             }
434         });
435     }
436 
437     // Called by Session2Link.onDisconnect
onDisconnect(@onNull final Controller2Link controller, int seq)438     void onDisconnect(@NonNull final Controller2Link controller, int seq) {
439         final ControllerInfo controllerInfo;
440         synchronized (mLock) {
441             controllerInfo = mConnectedControllers.remove(controller);
442         }
443         if (controllerInfo == null) {
444             return;
445         }
446         mCallbackExecutor.execute(() -> {
447             mCallback.onDisconnected(MediaSession2.this, controllerInfo);
448         });
449     }
450 
451     // Called by Session2Link.onSessionCommand
onSessionCommand(@onNull final Controller2Link controller, final int seq, final Session2Command command, final Bundle args, @Nullable ResultReceiver resultReceiver)452     void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
453             final Session2Command command, final Bundle args,
454             @Nullable ResultReceiver resultReceiver) {
455         if (controller == null) {
456             return;
457         }
458         final ControllerInfo controllerInfo;
459         synchronized (mLock) {
460             controllerInfo = mConnectedControllers.get(controller);
461         }
462         if (controllerInfo == null) {
463             return;
464         }
465 
466         // TODO: check allowed commands.
467         synchronized (mLock) {
468             controllerInfo.addRequestedCommandSeqNumber(seq);
469         }
470         mCallbackExecutor.execute(() -> {
471             if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) {
472                 if (resultReceiver != null) {
473                     resultReceiver.send(RESULT_INFO_SKIPPED, null);
474                 }
475                 return;
476             }
477             Session2Command.Result result = mCallback.onSessionCommand(
478                     MediaSession2.this, controllerInfo, command, args);
479             if (resultReceiver != null) {
480                 if (result == null) {
481                     resultReceiver.send(RESULT_INFO_SKIPPED, null);
482                 } else {
483                     resultReceiver.send(result.getResultCode(), result.getResultData());
484                 }
485             }
486         });
487     }
488 
489     // Called by Session2Link.onCancelCommand
onCancelCommand(@onNull final Controller2Link controller, final int seq)490     void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
491         final ControllerInfo controllerInfo;
492         synchronized (mLock) {
493             controllerInfo = mConnectedControllers.get(controller);
494         }
495         if (controllerInfo == null) {
496             return;
497         }
498         controllerInfo.removeRequestedCommandSeqNumber(seq);
499     }
500 
501     /**
502      * This API is not generally intended for third party application developers.
503      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
504      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
505      * Library</a> for consistent behavior across all devices.
506      * <p>
507      * Builder for {@link MediaSession2}.
508      * <p>
509      * Any incoming event from the {@link MediaController2} will be handled on the callback
510      * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
511      */
512     public static final class Builder {
513         private Context mContext;
514         private String mId;
515         private PendingIntent mSessionActivity;
516         private Executor mCallbackExecutor;
517         private SessionCallback mCallback;
518         private Bundle mExtras;
519 
520         /**
521          * Creates a builder for {@link MediaSession2}.
522          *
523          * @param context Context
524          * @throws IllegalArgumentException if context is {@code null}.
525          */
Builder(@onNull Context context)526         public Builder(@NonNull Context context) {
527             if (context == null) {
528                 throw new IllegalArgumentException("context shouldn't be null");
529             }
530             mContext = context;
531         }
532 
533         /**
534          * Set an intent for launching UI for this Session. This can be used as a
535          * quick link to an ongoing media screen. The intent should be for an
536          * activity that may be started using {@link Context#startActivity(Intent)}.
537          *
538          * @param pi The intent to launch to show UI for this session.
539          * @return The Builder to allow chaining
540          */
541         @NonNull
setSessionActivity(@ullable PendingIntent pi)542         public Builder setSessionActivity(@Nullable PendingIntent pi) {
543             mSessionActivity = pi;
544             return this;
545         }
546 
547         /**
548          * Set ID of the session. If it's not set, an empty string will be used to create a session.
549          * <p>
550          * Use this if and only if your app supports multiple playback at the same time and also
551          * wants to provide external apps to have finer controls of them.
552          *
553          * @param id id of the session. Must be unique per package.
554          * @throws IllegalArgumentException if id is {@code null}.
555          * @return The Builder to allow chaining
556          */
557         @NonNull
setId(@onNull String id)558         public Builder setId(@NonNull String id) {
559             if (id == null) {
560                 throw new IllegalArgumentException("id shouldn't be null");
561             }
562             mId = id;
563             return this;
564         }
565 
566         /**
567          * Set callback for the session and its executor.
568          *
569          * @param executor callback executor
570          * @param callback session callback.
571          * @return The Builder to allow chaining
572          */
573         @NonNull
setSessionCallback(@onNull Executor executor, @NonNull SessionCallback callback)574         public Builder setSessionCallback(@NonNull Executor executor,
575                 @NonNull SessionCallback callback) {
576             mCallbackExecutor = executor;
577             mCallback = callback;
578             return this;
579         }
580 
581         /**
582          * Set extras for the session token. If null or not set, {@link Session2Token#getExtras()}
583          * will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown
584          * if the bundle contains any non-framework Parcelable objects.
585          *
586          * @return The Builder to allow chaining
587          * @see Session2Token#getExtras()
588          */
589         @NonNull
setExtras(@onNull Bundle extras)590         public Builder setExtras(@NonNull Bundle extras) {
591             if (extras == null) {
592                 throw new NullPointerException("extras shouldn't be null");
593             }
594             if (hasCustomParcelable(extras)) {
595                 throw new IllegalArgumentException(
596                         "extras shouldn't contain any custom parcelables");
597             }
598             mExtras = new Bundle(extras);
599             return this;
600         }
601 
602         /**
603          * Build {@link MediaSession2}.
604          *
605          * @return a new session
606          * @throws IllegalStateException if the session with the same id is already exists for the
607          *      package.
608          */
609         @NonNull
build()610         public MediaSession2 build() {
611             if (mCallbackExecutor == null) {
612                 mCallbackExecutor = mContext.getMainExecutor();
613             }
614             if (mCallback == null) {
615                 mCallback = new SessionCallback() {};
616             }
617             if (mId == null) {
618                 mId = "";
619             }
620             if (mExtras == null) {
621                 mExtras = Bundle.EMPTY;
622             }
623             MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity,
624                     mCallbackExecutor, mCallback, mExtras);
625 
626             // Notify framework about the newly create session after the constructor is finished.
627             // Otherwise, framework may access the session before the initialization is finished.
628             try {
629                 if (SdkLevel.isAtLeastS()) {
630                     MediaCommunicationManager manager =
631                             mContext.getSystemService(MediaCommunicationManager.class);
632                     manager.notifySession2Created(session2.getToken());
633                 } else {
634                     MediaSessionManager manager =
635                             mContext.getSystemService(MediaSessionManager.class);
636                     manager.notifySession2Created(session2.getToken());
637                 }
638             } catch (Exception e) {
639                 session2.close();
640                 throw e;
641             }
642 
643             return session2;
644         }
645     }
646 
647     /**
648      * This API is not generally intended for third party application developers.
649      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
650      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
651      * Library</a> for consistent behavior across all devices.
652      * <p>
653      * Information of a controller.
654      */
655     public static final class ControllerInfo {
656         private final RemoteUserInfo mRemoteUserInfo;
657         private final boolean mIsTrusted;
658         private final Controller2Link mControllerBinder;
659         private final Bundle mConnectionHints;
660         private final Object mLock = new Object();
661         //@GuardedBy("mLock")
662         private int mNextSeqNumber;
663         //@GuardedBy("mLock")
664         private ArrayMap<ResultReceiver, Integer> mPendingCommands;
665         //@GuardedBy("mLock")
666         private ArraySet<Integer> mRequestedCommandSeqNumbers;
667 
668         @SuppressWarnings("WeakerAccess") /* synthetic access */
669         Session2CommandGroup mAllowedCommands;
670 
671         /**
672          * @param remoteUserInfo remote user info
673          * @param trusted {@code true} if trusted, {@code false} otherwise
674          * @param controllerBinder Controller2Link for the connected controller.
675          * @param connectionHints a session-specific argument sent from the controller for the
676          *                        connection. The contents of this bundle may affect the
677          *                        connection result.
678          */
ControllerInfo(@onNull RemoteUserInfo remoteUserInfo, boolean trusted, @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints)679         ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
680                 @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) {
681             mRemoteUserInfo = remoteUserInfo;
682             mIsTrusted = trusted;
683             mControllerBinder = controllerBinder;
684             mConnectionHints = connectionHints;
685             mPendingCommands = new ArrayMap<>();
686             mRequestedCommandSeqNumbers = new ArraySet<>();
687         }
688 
689         /**
690          * @return remote user info of the controller.
691          */
692         @NonNull
getRemoteUserInfo()693         public RemoteUserInfo getRemoteUserInfo() {
694             return mRemoteUserInfo;
695         }
696 
697         /**
698          * @return package name of the controller.
699          */
700         @NonNull
getPackageName()701         public String getPackageName() {
702             return mRemoteUserInfo.getPackageName();
703         }
704 
705         /**
706          * @return uid of the controller. Can be a negative value if the uid cannot be obtained.
707          */
getUid()708         public int getUid() {
709             return mRemoteUserInfo.getUid();
710         }
711 
712         /**
713          * @return connection hints sent from controller.
714          */
715         @NonNull
getConnectionHints()716         public Bundle getConnectionHints() {
717             return new Bundle(mConnectionHints);
718         }
719 
720         /**
721          * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
722          * has a enabled notification listener so can be trusted to accept connection and incoming
723          * command request.
724          *
725          * @return {@code true} if the controller is trusted.
726          * @hide
727          */
isTrusted()728         public boolean isTrusted() {
729             return mIsTrusted;
730         }
731 
732         @Override
hashCode()733         public int hashCode() {
734             return Objects.hash(mControllerBinder, mRemoteUserInfo);
735         }
736 
737         @Override
equals(@ullable Object obj)738         public boolean equals(@Nullable Object obj) {
739             if (!(obj instanceof ControllerInfo)) return false;
740             if (this == obj) return true;
741 
742             ControllerInfo other = (ControllerInfo) obj;
743             if (mControllerBinder != null || other.mControllerBinder != null) {
744                 return Objects.equals(mControllerBinder, other.mControllerBinder);
745             }
746             return mRemoteUserInfo.equals(other.mRemoteUserInfo);
747         }
748 
749         @Override
750         @NonNull
toString()751         public String toString() {
752             return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
753                     + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})";
754         }
755 
notifyConnected(Bundle connectionResult)756         void notifyConnected(Bundle connectionResult) {
757             if (mControllerBinder == null) return;
758 
759             try {
760                 mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult);
761             } catch (RuntimeException e) {
762                 // Controller may be died prematurely.
763             }
764         }
765 
notifyDisconnected()766         void notifyDisconnected() {
767             if (mControllerBinder == null) return;
768 
769             try {
770                 mControllerBinder.notifyDisconnected(getNextSeqNumber());
771             } catch (RuntimeException e) {
772                 // Controller may be died prematurely.
773             }
774         }
775 
notifyPlaybackActiveChanged(boolean playbackActive)776         void notifyPlaybackActiveChanged(boolean playbackActive) {
777             if (mControllerBinder == null) return;
778 
779             try {
780                 mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive);
781             } catch (RuntimeException e) {
782                 // Controller may be died prematurely.
783             }
784         }
785 
sendSessionCommand(Session2Command command, Bundle args, ResultReceiver resultReceiver)786         void sendSessionCommand(Session2Command command, Bundle args,
787                 ResultReceiver resultReceiver) {
788             if (mControllerBinder == null) return;
789 
790             try {
791                 int seq = getNextSeqNumber();
792                 synchronized (mLock) {
793                     mPendingCommands.put(resultReceiver, seq);
794                 }
795                 mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver);
796             } catch (RuntimeException e) {
797                 // Controller may be died prematurely.
798                 synchronized (mLock) {
799                     mPendingCommands.remove(resultReceiver);
800                 }
801                 resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
802             }
803         }
804 
cancelSessionCommand(@onNull Object token)805         void cancelSessionCommand(@NonNull Object token) {
806             if (mControllerBinder == null) return;
807             Integer seq;
808             synchronized (mLock) {
809                 seq = mPendingCommands.remove(token);
810             }
811             if (seq != null) {
812                 mControllerBinder.cancelSessionCommand(seq);
813             }
814         }
815 
receiveCommandResult(ResultReceiver resultReceiver)816         void receiveCommandResult(ResultReceiver resultReceiver) {
817             synchronized (mLock) {
818                 mPendingCommands.remove(resultReceiver);
819             }
820         }
821 
addRequestedCommandSeqNumber(int seq)822         void addRequestedCommandSeqNumber(int seq) {
823             synchronized (mLock) {
824                 mRequestedCommandSeqNumbers.add(seq);
825             }
826         }
827 
removeRequestedCommandSeqNumber(int seq)828         boolean removeRequestedCommandSeqNumber(int seq) {
829             synchronized (mLock) {
830                 return mRequestedCommandSeqNumbers.remove(seq);
831             }
832         }
833 
getNextSeqNumber()834         private int getNextSeqNumber() {
835             synchronized (mLock) {
836                 return mNextSeqNumber++;
837             }
838         }
839     }
840 
841     /**
842      * This API is not generally intended for third party application developers.
843      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
844      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
845      * Library</a> for consistent behavior across all devices.
846      * <p>
847      * Callback to be called for all incoming commands from {@link MediaController2}s.
848      */
849     public abstract static class SessionCallback {
850         /**
851          * Called when a controller is created for this session. Return allowed commands for
852          * controller. By default it returns {@code null}.
853          * <p>
854          * You can reject the connection by returning {@code null}. In that case, controller
855          * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
856          * and cannot be used.
857          * <p>
858          * The controller hasn't connected yet in this method, so calls to the controller
859          * (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for
860          * the custom initialization for the controller instead.
861          *
862          * @param session the session for this event
863          * @param controller controller information.
864          * @return allowed commands. Can be {@code null} to reject connection.
865          */
866         @Nullable
onConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)867         public Session2CommandGroup onConnect(@NonNull MediaSession2 session,
868                 @NonNull ControllerInfo controller) {
869             return null;
870         }
871 
872         /**
873          * Called immediately after a controller is connected. This is a convenient method to add
874          * custom initialization between the session and a controller.
875          * <p>
876          * Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't
877          * work in {@link #onConnect} because the controller hasn't connected yet in
878          * {@link #onConnect}.
879          *
880          * @param session the session for this event
881          * @param controller controller information.
882          */
onPostConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)883         public void onPostConnect(@NonNull MediaSession2 session,
884                 @NonNull ControllerInfo controller) {
885         }
886 
887         /**
888          * Called when a controller is disconnected
889          *
890          * @param session the session for this event
891          * @param controller controller information
892          */
onDisconnected(@onNull MediaSession2 session, @NonNull ControllerInfo controller)893         public void onDisconnected(@NonNull MediaSession2 session,
894                 @NonNull ControllerInfo controller) {}
895 
896         /**
897          * Called when a controller sent a session command.
898          *
899          * @param session the session for this event
900          * @param controller controller information
901          * @param command the session command
902          * @param args optional arguments
903          * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
904          *         will be sent to the session.
905          */
906         @Nullable
onSessionCommand(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Session2Command command, @Nullable Bundle args)907         public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session,
908                 @NonNull ControllerInfo controller, @NonNull Session2Command command,
909                 @Nullable Bundle args) {
910             return null;
911         }
912 
913         /**
914          * Called when the command sent to the controller is finished.
915          *
916          * @param session the session for this event
917          * @param controller controller information
918          * @param token the token got from {@link MediaSession2#sendSessionCommand}
919          * @param command the session command
920          * @param result the result of the session command
921          */
onCommandResult(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Object token, @NonNull Session2Command command, @NonNull Session2Command.Result result)922         public void onCommandResult(@NonNull MediaSession2 session,
923                 @NonNull ControllerInfo controller, @NonNull Object token,
924                 @NonNull Session2Command command, @NonNull Session2Command.Result result) {}
925     }
926 
927     abstract static class ForegroundServiceEventCallback {
onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive)928         public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {}
onSessionClosed(MediaSession2 session)929         public void onSessionClosed(MediaSession2 session) {}
930     }
931 }
932