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