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