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