• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 package android.media;
17 
18 import static android.Manifest.permission.MEDIA_CONTENT_CONTROL;
19 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
20 
21 import android.annotation.CallbackExecutor;
22 import android.annotation.IntRange;
23 import android.annotation.NonNull;
24 import android.annotation.RequiresPermission;
25 import android.annotation.SystemApi;
26 import android.annotation.SystemService;
27 import android.content.Context;
28 import android.media.session.MediaSession;
29 import android.media.session.MediaSessionManager;
30 import android.os.Build;
31 import android.os.RemoteException;
32 import android.os.UserHandle;
33 import android.service.media.MediaBrowserService;
34 import android.util.Log;
35 import android.view.KeyEvent;
36 
37 import androidx.annotation.RequiresApi;
38 
39 import androidx.annotation.RequiresApi;
40 
41 import com.android.internal.annotations.GuardedBy;
42 import com.android.modules.annotation.MinSdk;
43 import com.android.modules.utils.build.SdkLevel;
44 
45 import java.util.Collections;
46 import java.util.List;
47 import java.util.Objects;
48 import java.util.concurrent.CopyOnWriteArrayList;
49 import java.util.concurrent.Executor;
50 
51 /**
52  * Provides support for interacting with {@link android.media.MediaSession2 MediaSession2s}
53  * that applications have published to express their ongoing media playback state.
54  */
55 @MinSdk(Build.VERSION_CODES.S)
56 @RequiresApi(Build.VERSION_CODES.S)
57 @SystemService(Context.MEDIA_COMMUNICATION_SERVICE)
58 public class MediaCommunicationManager {
59     private static final String TAG = "MediaCommunicationManager";
60 
61     /**
62      * The manager version used from beginning.
63      */
64     private static final int VERSION_1 = 1;
65 
66     /**
67      * Current manager version.
68      */
69     private static final int CURRENT_VERSION = VERSION_1;
70 
71     private final Context mContext;
72     // Do not access directly use getService().
73     private IMediaCommunicationService mService;
74 
75     private final Object mLock = new Object();
76     private final CopyOnWriteArrayList<SessionCallbackRecord> mTokenCallbackRecords =
77             new CopyOnWriteArrayList<>();
78 
79     @GuardedBy("mLock")
80     private MediaCommunicationServiceCallbackStub mCallbackStub;
81 
82     // TODO: remove this when MCS implements dispatchMediaKeyEvent.
83     private MediaSessionManager mMediaSessionManager;
84 
85     /**
86      * @hide
87      */
MediaCommunicationManager(@onNull Context context)88     public MediaCommunicationManager(@NonNull Context context) {
89         if (!SdkLevel.isAtLeastS()) {
90             throw new UnsupportedOperationException("Android version must be S or greater.");
91         }
92         mContext = context;
93     }
94 
95     /**
96      * Gets the version of this {@link MediaCommunicationManager}.
97      */
getVersion()98     public @IntRange(from = 1) int getVersion() {
99         return CURRENT_VERSION;
100     }
101 
102     /**
103      * Notifies that a new {@link MediaSession2} with type {@link Session2Token#TYPE_SESSION} is
104      * created.
105      * @param token newly created session2 token
106      * @hide
107      */
notifySession2Created(@onNull Session2Token token)108     public void notifySession2Created(@NonNull Session2Token token) {
109         Objects.requireNonNull(token, "token shouldn't be null");
110         if (token.getType() != Session2Token.TYPE_SESSION) {
111             throw new IllegalArgumentException("token's type should be TYPE_SESSION");
112         }
113         try {
114             getService().notifySession2Created(token);
115         } catch (RemoteException e) {
116             e.rethrowFromSystemServer();
117         }
118     }
119 
120     /**
121      * Checks whether the remote user is a trusted app.
122      * <p>
123      * An app is trusted if the app holds the
124      * {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission or has an enabled
125      * notification listener.
126      *
127      * @param userInfo The remote user info from either
128      *            {@link MediaSession#getCurrentControllerInfo()} or
129      *            {@link MediaBrowserService#getCurrentBrowserInfo()}.
130      * @return {@code true} if the remote user is trusted or {@code false} otherwise.
131      * @hide
132      */
isTrustedForMediaControl(@onNull MediaSessionManager.RemoteUserInfo userInfo)133     public boolean isTrustedForMediaControl(@NonNull MediaSessionManager.RemoteUserInfo userInfo) {
134         Objects.requireNonNull(userInfo, "userInfo shouldn't be null");
135         if (userInfo.getPackageName() == null) {
136             return false;
137         }
138         try {
139             return getService().isTrusted(
140                     userInfo.getPackageName(), userInfo.getPid(), userInfo.getUid());
141         } catch (RemoteException e) {
142             Log.w(TAG, "Cannot communicate with the service.", e);
143         }
144         return false;
145     }
146 
147     /**
148      * This API is not generally intended for third party application developers.
149      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
150      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
151      * Library</a> for consistent behavior across all devices.
152      * <p>
153      * Gets a list of {@link Session2Token} with type {@link Session2Token#TYPE_SESSION} for the
154      * current user.
155      * <p>
156      * Although this API can be used without any restriction, each session owners can accept or
157      * reject your uses of {@link MediaSession2}.
158      *
159      * @return A list of {@link Session2Token}.
160      */
161     @NonNull
getSession2Tokens()162     public List<Session2Token> getSession2Tokens() {
163         return getSession2Tokens(UserHandle.myUserId());
164     }
165 
166     /**
167      * Adds a callback to be notified when the list of active sessions changes.
168      * <p>
169      * This requires the {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission be
170      * held by the calling app.
171      * </p>
172      * @hide
173      */
174     @SystemApi(client = MODULE_LIBRARIES)
175     @RequiresPermission(MEDIA_CONTENT_CONTROL)
registerSessionCallback(@allbackExecutor @onNull Executor executor, @NonNull SessionCallback callback)176     public void registerSessionCallback(@CallbackExecutor @NonNull Executor executor,
177             @NonNull SessionCallback callback) {
178         Objects.requireNonNull(executor, "executor must not be null");
179         Objects.requireNonNull(callback, "callback must not be null");
180 
181         if (!mTokenCallbackRecords.addIfAbsent(
182                 new SessionCallbackRecord(executor, callback))) {
183             Log.w(TAG, "registerSession2TokenCallback: Ignoring the same callback");
184             return;
185         }
186         synchronized (mLock) {
187             if (mCallbackStub == null) {
188                 MediaCommunicationServiceCallbackStub callbackStub =
189                         new MediaCommunicationServiceCallbackStub();
190                 try {
191                     getService().registerCallback(callbackStub, mContext.getPackageName());
192                     mCallbackStub = callbackStub;
193                 } catch (RemoteException ex) {
194                     Log.e(TAG, "Failed to register callback.", ex);
195                 }
196             }
197         }
198     }
199 
200     /**
201      * Stops receiving active sessions updates on the specified callback.
202      * @hide
203      */
204     @SystemApi(client = MODULE_LIBRARIES)
unregisterSessionCallback(@onNull SessionCallback callback)205     public void unregisterSessionCallback(@NonNull SessionCallback callback) {
206         if (!mTokenCallbackRecords.remove(
207                 new SessionCallbackRecord(null, callback))) {
208             Log.w(TAG, "unregisterSession2TokenCallback: Ignoring an unknown callback.");
209             return;
210         }
211         synchronized (mLock) {
212             if (mCallbackStub != null && mTokenCallbackRecords.isEmpty()) {
213                 try {
214                     getService().unregisterCallback(mCallbackStub);
215                 } catch (RemoteException ex) {
216                     Log.e(TAG, "Failed to unregister callback.", ex);
217                 }
218                 mCallbackStub = null;
219             }
220         }
221     }
222 
getService()223     private IMediaCommunicationService getService() {
224         if (mService == null) {
225             mService = IMediaCommunicationService.Stub.asInterface(
226                     MediaFrameworkInitializer.getMediaServiceManager()
227                             .getMediaCommunicationServiceRegisterer()
228                             .get());
229         }
230         return mService;
231     }
232 
233     // TODO: remove this when MCS implements dispatchMediaKeyEvent.
getMediaSessionManager()234     private MediaSessionManager getMediaSessionManager() {
235         if (mMediaSessionManager == null) {
236             mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
237         }
238         return mMediaSessionManager;
239     }
240 
getSession2Tokens(int userId)241     private List<Session2Token> getSession2Tokens(int userId) {
242         try {
243             MediaParceledListSlice slice = getService().getSession2Tokens(userId);
244             return slice == null ? Collections.emptyList() : slice.getList();
245         } catch (RemoteException e) {
246             Log.e(TAG, "Failed to get session tokens", e);
247         }
248         return Collections.emptyList();
249     }
250 
251     /**
252      * Sends a media key event. The receiver will be selected automatically.
253      *
254      * @param keyEvent the key event to send, non-media key events will be ignored.
255      * @param asSystemService if {@code true}, the event is sent to the session as if it was come
256      *                        from the system service instead of the app process. It only affects
257      *                        {@link MediaSession.Callback#getCurrentControllerInfo()}.
258      * @hide
259      */
260     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
dispatchMediaKeyEvent(@onNull KeyEvent keyEvent, boolean asSystemService)261     public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent, boolean asSystemService) {
262         Objects.requireNonNull(keyEvent, "keyEvent shouldn't be null");
263 
264         // When MCS handles this, caller is changed.
265         // TODO: remove this when MCS implementation is done.
266         if (!asSystemService) {
267             getMediaSessionManager().dispatchMediaKeyEvent(keyEvent, false);
268             return;
269         }
270 
271         try {
272             getService().dispatchMediaKeyEvent(mContext.getPackageName(),
273                     keyEvent, asSystemService);
274         } catch (RemoteException e) {
275             Log.e(TAG, "Failed to send key event.", e);
276         }
277     }
278 
279     /**
280      * Callback for listening to changes to the sessions.
281      * @see #registerSessionCallback(Executor, SessionCallback)
282      * @hide
283      */
284     @SystemApi(client = MODULE_LIBRARIES)
285     public interface SessionCallback {
286         /**
287          * Called when a new {@link MediaSession2 media session2} is created.
288          * @param token the newly created token
289          */
onSession2TokenCreated(@onNull Session2Token token)290         default void onSession2TokenCreated(@NonNull Session2Token token) {}
291 
292         /**
293          * Called when {@link #getSession2Tokens() session tokens} are changed.
294          */
onSession2TokensChanged(@onNull List<Session2Token> tokens)295         default void onSession2TokensChanged(@NonNull List<Session2Token> tokens) {}
296     }
297 
298     private static final class SessionCallbackRecord {
299         public final Executor executor;
300         public final SessionCallback callback;
301 
SessionCallbackRecord(Executor executor, SessionCallback callback)302         SessionCallbackRecord(Executor executor, SessionCallback callback) {
303             this.executor = executor;
304             this.callback = callback;
305         }
306 
307         @Override
hashCode()308         public int hashCode() {
309             return Objects.hash(callback);
310         }
311 
312         @Override
equals(Object obj)313         public boolean equals(Object obj) {
314             if (this == obj) {
315                 return true;
316             }
317             if (!(obj instanceof SessionCallbackRecord)) {
318                 return false;
319             }
320             return Objects.equals(this.callback, ((SessionCallbackRecord) obj).callback);
321         }
322     }
323 
324     class MediaCommunicationServiceCallbackStub extends IMediaCommunicationServiceCallback.Stub {
325         @Override
onSession2Created(Session2Token token)326         public void onSession2Created(Session2Token token) throws RemoteException {
327             for (SessionCallbackRecord record : mTokenCallbackRecords) {
328                 record.executor.execute(() -> record.callback.onSession2TokenCreated(token));
329             }
330         }
331 
332         @Override
onSession2Changed(MediaParceledListSlice tokens)333         public void onSession2Changed(MediaParceledListSlice tokens) throws RemoteException {
334             List<Session2Token> tokenList = tokens.getList();
335             for (SessionCallbackRecord record : mTokenCallbackRecords) {
336                 record.executor.execute(() -> record.callback.onSession2TokensChanged(tokenList));
337             }
338         }
339     }
340 }
341