| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.media; |
| |
| import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; |
| import static android.content.Intent.ACTION_SCREEN_OFF; |
| import static android.content.Intent.ACTION_SCREEN_ON; |
| import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; |
| import static android.media.MediaRouter2Utils.getOriginalId; |
| import static android.media.MediaRouter2Utils.getProviderId; |
| |
| import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.app.ActivityManager; |
| import android.app.ActivityThread; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.media.IMediaRouter2; |
| import android.media.IMediaRouter2Manager; |
| import android.media.MediaRoute2Info; |
| import android.media.MediaRoute2ProviderInfo; |
| import android.media.MediaRoute2ProviderService; |
| import android.media.MediaRouter2Manager; |
| import android.media.RouteDiscoveryPreference; |
| import android.media.RouteListingPreference; |
| import android.media.RoutingSessionInfo; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.DeviceConfig; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.function.pooled.PooledLambda; |
| import com.android.server.LocalServices; |
| import com.android.server.pm.UserManagerInternal; |
| |
| import java.io.PrintWriter; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Implements features related to {@link android.media.MediaRouter2} and |
| * {@link android.media.MediaRouter2Manager}. |
| */ |
| class MediaRouter2ServiceImpl { |
| private static final String TAG = "MR2ServiceImpl"; |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| // TODO: (In Android S or later) if we add callback methods for generic failures |
| // in MediaRouter2, remove this constant and replace the usages with the real request IDs. |
| private static final long DUMMY_REQUEST_ID = -1; |
| |
| private static final String MEDIA_BETTER_TOGETHER_NAMESPACE = "media_better_together"; |
| |
| private static final String KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE = |
| "scanning_package_minimum_importance"; |
| |
| /** |
| * Contains the list of bluetooth permissions that are required to do system routing. |
| * |
| * <p>Alternatively, apps that hold {@link android.Manifest.permission#MODIFY_AUDIO_ROUTING} are |
| * also allowed to do system routing. |
| */ |
| private static final String[] BLUETOOTH_PERMISSIONS_FOR_SYSTEM_ROUTING = |
| new String[] { |
| Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN |
| }; |
| |
| private static int sPackageImportanceForScanning = DeviceConfig.getInt( |
| MEDIA_BETTER_TOGETHER_NAMESPACE, |
| /* name */ KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE, |
| /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE); |
| |
| private final Context mContext; |
| private final UserManagerInternal mUserManagerInternal; |
| private final Object mLock = new Object(); |
| final AtomicInteger mNextRouterOrManagerId = new AtomicInteger(1); |
| final ActivityManager mActivityManager; |
| final PowerManager mPowerManager; |
| |
| @GuardedBy("mLock") |
| private final SparseArray<UserRecord> mUserRecords = new SparseArray<>(); |
| @GuardedBy("mLock") |
| private final ArrayMap<IBinder, RouterRecord> mAllRouterRecords = new ArrayMap<>(); |
| @GuardedBy("mLock") |
| private final ArrayMap<IBinder, ManagerRecord> mAllManagerRecords = new ArrayMap<>(); |
| @GuardedBy("mLock") |
| private int mCurrentActiveUserId = -1; |
| |
| private final ActivityManager.OnUidImportanceListener mOnUidImportanceListener = |
| (uid, importance) -> { |
| synchronized (mLock) { |
| final int count = mUserRecords.size(); |
| for (int i = 0; i < count; i++) { |
| mUserRecords.valueAt(i).mHandler.maybeUpdateDiscoveryPreferenceForUid(uid); |
| } |
| } |
| }; |
| |
| private final BroadcastReceiver mScreenOnOffReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| synchronized (mLock) { |
| final int count = mUserRecords.size(); |
| for (int i = 0; i < count; i++) { |
| UserHandler userHandler = mUserRecords.valueAt(i).mHandler; |
| userHandler.sendMessage(PooledLambda.obtainMessage( |
| UserHandler::updateDiscoveryPreferenceOnHandler, userHandler)); |
| } |
| } |
| } |
| }; |
| |
| @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) |
| /* package */ MediaRouter2ServiceImpl(Context context) { |
| mContext = context; |
| mActivityManager = mContext.getSystemService(ActivityManager.class); |
| mActivityManager.addOnUidImportanceListener(mOnUidImportanceListener, |
| sPackageImportanceForScanning); |
| mPowerManager = mContext.getSystemService(PowerManager.class); |
| mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); |
| |
| IntentFilter screenOnOffIntentFilter = new IntentFilter(); |
| screenOnOffIntentFilter.addAction(ACTION_SCREEN_ON); |
| screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF); |
| |
| mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); |
| mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged); |
| |
| DeviceConfig.addOnPropertiesChangedListener(MEDIA_BETTER_TOGETHER_NAMESPACE, |
| ActivityThread.currentApplication().getMainExecutor(), |
| this::onDeviceConfigChange); |
| } |
| |
| /** |
| * Called when there's a change in the permissions of an app. |
| * |
| * @param uid The uid of the app whose permissions changed. |
| */ |
| private void onPermissionsChanged(int uid) { |
| synchronized (mLock) { |
| Optional<RouterRecord> affectedRouter = |
| mAllRouterRecords.values().stream().filter(it -> it.mUid == uid).findFirst(); |
| if (affectedRouter.isPresent()) { |
| affectedRouter.get().maybeUpdateSystemRoutingPermissionLocked(); |
| } |
| } |
| } |
| |
| // Start of methods that implement MediaRouter2 operations. |
| |
| @NonNull |
| public boolean verifyPackageExists(@NonNull String clientPackageName) { |
| final int pid = Binder.getCallingPid(); |
| final int uid = Binder.getCallingUid(); |
| final long token = Binder.clearCallingIdentity(); |
| |
| try { |
| mContext.enforcePermission( |
| Manifest.permission.MEDIA_CONTENT_CONTROL, |
| pid, |
| uid, |
| "Must hold MEDIA_CONTENT_CONTROL permission."); |
| PackageManager pm = mContext.getPackageManager(); |
| pm.getPackageInfo(clientPackageName, PackageManager.PackageInfoFlags.of(0)); |
| return true; |
| } catch (PackageManager.NameNotFoundException ex) { |
| return false; |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @NonNull |
| public List<MediaRoute2Info> getSystemRoutes() { |
| final int uid = Binder.getCallingUid(); |
| final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); |
| final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission( |
| android.Manifest.permission.MODIFY_AUDIO_ROUTING) |
| == PackageManager.PERMISSION_GRANTED; |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| Collection<MediaRoute2Info> systemRoutes; |
| synchronized (mLock) { |
| UserRecord userRecord = getOrCreateUserRecordLocked(userId); |
| if (hasModifyAudioRoutingPermission) { |
| MediaRoute2ProviderInfo providerInfo = |
| userRecord.mHandler.mSystemProvider.getProviderInfo(); |
| if (providerInfo != null) { |
| systemRoutes = providerInfo.getRoutes(); |
| } else { |
| systemRoutes = Collections.emptyList(); |
| } |
| } else { |
| systemRoutes = new ArrayList<>(); |
| systemRoutes.add(userRecord.mHandler.mSystemProvider.getDefaultRoute()); |
| } |
| } |
| return new ArrayList<>(systemRoutes); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void registerRouter2(@NonNull IMediaRouter2 router, @NonNull String packageName) { |
| Objects.requireNonNull(router, "router must not be null"); |
| if (TextUtils.isEmpty(packageName)) { |
| throw new IllegalArgumentException("packageName must not be empty"); |
| } |
| |
| final int uid = Binder.getCallingUid(); |
| final int pid = Binder.getCallingPid(); |
| final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); |
| final boolean hasConfigureWifiDisplayPermission = mContext.checkCallingOrSelfPermission( |
| android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) |
| == PackageManager.PERMISSION_GRANTED; |
| final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission( |
| android.Manifest.permission.MODIFY_AUDIO_ROUTING) |
| == PackageManager.PERMISSION_GRANTED; |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| registerRouter2Locked(router, uid, pid, packageName, userId, |
| hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void unregisterRouter2(@NonNull IMediaRouter2 router) { |
| Objects.requireNonNull(router, "router must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| unregisterRouter2Locked(router, false); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void setDiscoveryRequestWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull RouteDiscoveryPreference preference) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(preference, "preference must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder()); |
| if (routerRecord == null) { |
| Slog.w(TAG, "Ignoring updating discoveryRequest of null routerRecord."); |
| return; |
| } |
| setDiscoveryRequestWithRouter2Locked(routerRecord, preference); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void setRouteListingPreference( |
| @NonNull IMediaRouter2 router, |
| @Nullable RouteListingPreference routeListingPreference) { |
| ComponentName linkedItemLandingComponent = |
| routeListingPreference != null |
| ? routeListingPreference.getLinkedItemComponentName() |
| : null; |
| if (linkedItemLandingComponent != null) { |
| int callingUid = Binder.getCallingUid(); |
| MediaServerUtils.enforcePackageName( |
| linkedItemLandingComponent.getPackageName(), callingUid); |
| if (!MediaServerUtils.isValidActivityComponentName( |
| mContext, |
| linkedItemLandingComponent, |
| RouteListingPreference.ACTION_TRANSFER_MEDIA, |
| Binder.getCallingUserHandle())) { |
| throw new IllegalArgumentException( |
| "Unable to resolve " |
| + linkedItemLandingComponent |
| + " to a valid activity for " |
| + RouteListingPreference.ACTION_TRANSFER_MEDIA); |
| } |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder()); |
| if (routerRecord == null) { |
| Slog.w(TAG, "Ignoring updating route listing of null routerRecord."); |
| return; |
| } |
| setRouteListingPreferenceLocked(routerRecord, routeListingPreference); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void setRouteVolumeWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull MediaRoute2Info route, int volume) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| setRouteVolumeWithRouter2Locked(router, route, volume); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void requestCreateSessionWithRouter2(@NonNull IMediaRouter2 router, int requestId, |
| long managerRequestId, @NonNull RoutingSessionInfo oldSession, |
| @NonNull MediaRoute2Info route, Bundle sessionHints) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(oldSession, "oldSession must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| requestCreateSessionWithRouter2Locked(requestId, managerRequestId, |
| router, oldSession, route, sessionHints); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void selectRouteWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| selectRouteWithRouter2Locked(router, uniqueSessionId, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void deselectRouteWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| deselectRouteWithRouter2Locked(router, uniqueSessionId, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void transferToRouteWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| transferToRouteWithRouter2Locked(router, uniqueSessionId, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void setSessionVolumeWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, int volume) { |
| Objects.requireNonNull(router, "router must not be null"); |
| Objects.requireNonNull(uniqueSessionId, "uniqueSessionId must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| setSessionVolumeWithRouter2Locked(router, uniqueSessionId, volume); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void releaseSessionWithRouter2(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId) { |
| Objects.requireNonNull(router, "router must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| releaseSessionWithRouter2Locked(router, uniqueSessionId); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| // End of methods that implement MediaRouter2 operations. |
| |
| // Start of methods that implement MediaRouter2Manager operations. |
| |
| @NonNull |
| public List<RoutingSessionInfo> getRemoteSessions(@NonNull IMediaRouter2Manager manager) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| return getRemoteSessionsLocked(manager); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void registerManager(@NonNull IMediaRouter2Manager manager, |
| @NonNull String packageName) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| if (TextUtils.isEmpty(packageName)) { |
| throw new IllegalArgumentException("packageName must not be empty"); |
| } |
| |
| final int uid = Binder.getCallingUid(); |
| final int pid = Binder.getCallingPid(); |
| final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| registerManagerLocked(manager, uid, pid, packageName, userId); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void unregisterManager(@NonNull IMediaRouter2Manager manager) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| unregisterManagerLocked(manager, false); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void startScan(@NonNull IMediaRouter2Manager manager) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| startScanLocked(manager); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void stopScan(@NonNull IMediaRouter2Manager manager) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| stopScanLocked(manager); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void setRouteVolumeWithManager(@NonNull IMediaRouter2Manager manager, int requestId, |
| @NonNull MediaRoute2Info route, int volume) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| setRouteVolumeWithManagerLocked(requestId, manager, route, volume); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void requestCreateSessionWithManager(@NonNull IMediaRouter2Manager manager, |
| int requestId, @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| Objects.requireNonNull(oldSession, "oldSession must not be null"); |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void selectRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| selectRouteWithManagerLocked(requestId, manager, uniqueSessionId, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void deselectRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| deselectRouteWithManagerLocked(requestId, manager, uniqueSessionId, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void transferToRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| Objects.requireNonNull(route, "route must not be null"); |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| transferToRouteWithManagerLocked(requestId, manager, uniqueSessionId, route); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void setSessionVolumeWithManager(@NonNull IMediaRouter2Manager manager, int requestId, |
| @NonNull String uniqueSessionId, int volume) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| setSessionVolumeWithManagerLocked(requestId, manager, uniqueSessionId, volume); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public void releaseSessionWithManager(@NonNull IMediaRouter2Manager manager, int requestId, |
| @NonNull String uniqueSessionId) { |
| Objects.requireNonNull(manager, "manager must not be null"); |
| if (TextUtils.isEmpty(uniqueSessionId)) { |
| throw new IllegalArgumentException("uniqueSessionId must not be empty"); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mLock) { |
| releaseSessionWithManagerLocked(requestId, manager, uniqueSessionId); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| // End of methods that implement MediaRouter2Manager operations. |
| |
| // Start of methods that implements operations for both MediaRouter2 and MediaRouter2Manager. |
| |
| @Nullable |
| public RoutingSessionInfo getSystemSessionInfo( |
| @Nullable String packageName, boolean setDeviceRouteSelected) { |
| final int uid = Binder.getCallingUid(); |
| final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); |
| final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission( |
| android.Manifest.permission.MODIFY_AUDIO_ROUTING) |
| == PackageManager.PERMISSION_GRANTED; |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| RoutingSessionInfo systemSessionInfo = null; |
| synchronized (mLock) { |
| UserRecord userRecord = getOrCreateUserRecordLocked(userId); |
| List<RoutingSessionInfo> sessionInfos; |
| if (hasModifyAudioRoutingPermission) { |
| if (setDeviceRouteSelected) { |
| systemSessionInfo = userRecord.mHandler.mSystemProvider |
| .generateDeviceRouteSelectedSessionInfo(packageName); |
| } else { |
| sessionInfos = userRecord.mHandler.mSystemProvider.getSessionInfos(); |
| if (sessionInfos != null && !sessionInfos.isEmpty()) { |
| systemSessionInfo = new RoutingSessionInfo.Builder(sessionInfos.get(0)) |
| .setClientPackageName(packageName).build(); |
| } else { |
| Slog.w(TAG, "System provider does not have any session info."); |
| } |
| } |
| } else { |
| systemSessionInfo = new RoutingSessionInfo.Builder( |
| userRecord.mHandler.mSystemProvider.getDefaultSessionInfo()) |
| .setClientPackageName(packageName).build(); |
| } |
| } |
| return systemSessionInfo; |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| // End of methods that implements operations for both MediaRouter2 and MediaRouter2Manager. |
| |
| public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { |
| pw.println(prefix + "MediaRouter2ServiceImpl"); |
| |
| String indent = prefix + " "; |
| |
| synchronized (mLock) { |
| pw.println(indent + "mNextRouterOrManagerId=" + mNextRouterOrManagerId.get()); |
| pw.println(indent + "mCurrentActiveUserId=" + mCurrentActiveUserId); |
| |
| pw.println(indent + "UserRecords:"); |
| if (mUserRecords.size() > 0) { |
| for (int i = 0; i < mUserRecords.size(); i++) { |
| mUserRecords.valueAt(i).dump(pw, indent + " "); |
| } |
| } else { |
| pw.println(indent + " <no user records>"); |
| } |
| } |
| } |
| |
| /* package */ void updateRunningUserAndProfiles(int newActiveUserId) { |
| synchronized (mLock) { |
| if (mCurrentActiveUserId != newActiveUserId) { |
| Slog.i(TAG, TextUtils.formatSimple( |
| "switchUser | user: %d", newActiveUserId)); |
| |
| mCurrentActiveUserId = newActiveUserId; |
| // disposeUserIfNeededLocked might modify the collection, hence clone |
| final var userRecords = mUserRecords.clone(); |
| for (int i = 0; i < userRecords.size(); i++) { |
| int userId = userRecords.keyAt(i); |
| UserRecord userRecord = userRecords.valueAt(i); |
| if (isUserActiveLocked(userId)) { |
| // userId corresponds to the active user, or one of its profiles. We |
| // ensure the associated structures are initialized. |
| userRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::start, userRecord.mHandler)); |
| } else { |
| userRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::stop, userRecord.mHandler)); |
| disposeUserIfNeededLocked(userRecord); |
| } |
| } |
| } |
| } |
| } |
| |
| void routerDied(@NonNull RouterRecord routerRecord) { |
| synchronized (mLock) { |
| unregisterRouter2Locked(routerRecord.mRouter, true); |
| } |
| } |
| |
| void managerDied(@NonNull ManagerRecord managerRecord) { |
| synchronized (mLock) { |
| unregisterManagerLocked(managerRecord.mManager, true); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the given {@code userId} corresponds to the active user or a profile |
| * of the active user, returns {@code false} otherwise. |
| */ |
| @GuardedBy("mLock") |
| private boolean isUserActiveLocked(int userId) { |
| return mUserManagerInternal.getProfileParentId(userId) == mCurrentActiveUserId; |
| } |
| |
| // Start of locked methods that are used by MediaRouter2. |
| |
| @GuardedBy("mLock") |
| private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid, |
| @NonNull String packageName, int userId, boolean hasConfigureWifiDisplayPermission, |
| boolean hasModifyAudioRoutingPermission) { |
| final IBinder binder = router.asBinder(); |
| if (mAllRouterRecords.get(binder) != null) { |
| Slog.w(TAG, "registerRouter2Locked: Same router already exists. packageName=" |
| + packageName); |
| return; |
| } |
| |
| UserRecord userRecord = getOrCreateUserRecordLocked(userId); |
| RouterRecord routerRecord = new RouterRecord(userRecord, router, uid, pid, packageName, |
| hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission); |
| try { |
| binder.linkToDeath(routerRecord, 0); |
| } catch (RemoteException ex) { |
| throw new RuntimeException("MediaRouter2 died prematurely.", ex); |
| } |
| |
| userRecord.mRouterRecords.add(routerRecord); |
| mAllRouterRecords.put(binder, routerRecord); |
| |
| userRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::notifyRouterRegistered, |
| userRecord.mHandler, routerRecord)); |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "registerRouter2 | package: %s, uid: %d, pid: %d, router id: %d", |
| packageName, uid, pid, routerRecord.mRouterId)); |
| } |
| |
| @GuardedBy("mLock") |
| private void unregisterRouter2Locked(@NonNull IMediaRouter2 router, boolean died) { |
| RouterRecord routerRecord = mAllRouterRecords.remove(router.asBinder()); |
| if (routerRecord == null) { |
| Slog.w(TAG, "Ignoring unregistering unknown router2"); |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "unregisterRouter2 | package: %s, router id: %d", |
| routerRecord.mPackageName, routerRecord.mRouterId)); |
| |
| UserRecord userRecord = routerRecord.mUserRecord; |
| userRecord.mRouterRecords.remove(routerRecord); |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::notifyDiscoveryPreferenceChangedToManagers, |
| routerRecord.mUserRecord.mHandler, |
| routerRecord.mPackageName, null)); |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage( |
| UserHandler::notifyRouteListingPreferenceChangeToManagers, |
| routerRecord.mUserRecord.mHandler, |
| routerRecord.mPackageName, |
| /* routeListingPreference= */ null)); |
| userRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::updateDiscoveryPreferenceOnHandler, |
| userRecord.mHandler)); |
| routerRecord.dispose(); |
| disposeUserIfNeededLocked(userRecord); // since router removed from user |
| } |
| |
| private void setDiscoveryRequestWithRouter2Locked(@NonNull RouterRecord routerRecord, |
| @NonNull RouteDiscoveryPreference discoveryRequest) { |
| if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) { |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "setDiscoveryRequestWithRouter2 | router: %s(id: %d), discovery request:" |
| + " %s", |
| routerRecord.mPackageName, |
| routerRecord.mRouterId, |
| discoveryRequest.toString())); |
| |
| routerRecord.mDiscoveryPreference = discoveryRequest; |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::notifyDiscoveryPreferenceChangedToManagers, |
| routerRecord.mUserRecord.mHandler, |
| routerRecord.mPackageName, |
| routerRecord.mDiscoveryPreference)); |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::updateDiscoveryPreferenceOnHandler, |
| routerRecord.mUserRecord.mHandler)); |
| } |
| |
| @GuardedBy("mLock") |
| private void setRouteListingPreferenceLocked( |
| RouterRecord routerRecord, @Nullable RouteListingPreference routeListingPreference) { |
| routerRecord.mRouteListingPreference = routeListingPreference; |
| String routeListingAsString = |
| routeListingPreference != null |
| ? routeListingPreference.getItems().stream() |
| .map(RouteListingPreference.Item::getRouteId) |
| .collect(Collectors.joining(",")) |
| : null; |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "setRouteListingPreference | router: %s(id: %d), route listing preference:" |
| + " [%s]", |
| routerRecord.mPackageName, routerRecord.mRouterId, routeListingAsString)); |
| |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage( |
| UserHandler::notifyRouteListingPreferenceChangeToManagers, |
| routerRecord.mUserRecord.mHandler, |
| routerRecord.mPackageName, |
| routeListingPreference)); |
| } |
| |
| private void setRouteVolumeWithRouter2Locked(@NonNull IMediaRouter2 router, |
| @NonNull MediaRoute2Info route, int volume) { |
| final IBinder binder = router.asBinder(); |
| RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord != null) { |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "setRouteVolumeWithRouter2 | router: %s(id: %d), volume: %d", |
| routerRecord.mPackageName, routerRecord.mRouterId, volume)); |
| |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::setRouteVolumeOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| DUMMY_REQUEST_ID, route, volume)); |
| } |
| } |
| |
| private void requestCreateSessionWithRouter2Locked(int requestId, long managerRequestId, |
| @NonNull IMediaRouter2 router, @NonNull RoutingSessionInfo oldSession, |
| @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { |
| final IBinder binder = router.asBinder(); |
| final RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord == null) { |
| return; |
| } |
| |
| if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) { |
| ManagerRecord manager = routerRecord.mUserRecord.mHandler.findManagerWithId( |
| toRequesterId(managerRequestId)); |
| if (manager == null || manager.mLastSessionCreationRequest == null) { |
| Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " |
| + "Ignoring unknown request."); |
| routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( |
| routerRecord, requestId); |
| return; |
| } |
| if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(), |
| oldSession.getId())) { |
| Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " |
| + "Ignoring unmatched routing session."); |
| routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( |
| routerRecord, requestId); |
| return; |
| } |
| if (!TextUtils.equals(manager.mLastSessionCreationRequest.mRoute.getId(), |
| route.getId())) { |
| // When media router has no permission |
| if (!routerRecord.hasSystemRoutingPermission() |
| && manager.mLastSessionCreationRequest.mRoute.isSystemRoute() |
| && route.isSystemRoute()) { |
| route = manager.mLastSessionCreationRequest.mRoute; |
| } else { |
| Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " |
| + "Ignoring unmatched route."); |
| routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( |
| routerRecord, requestId); |
| return; |
| } |
| } |
| manager.mLastSessionCreationRequest = null; |
| } else { |
| if (route.isSystemRoute() |
| && !routerRecord.hasSystemRoutingPermission() |
| && !TextUtils.equals( |
| route.getId(), |
| routerRecord |
| .mUserRecord |
| .mHandler |
| .mSystemProvider |
| .getDefaultRoute() |
| .getId())) { |
| Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to" |
| + route); |
| routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( |
| routerRecord, requestId); |
| return; |
| } |
| } |
| |
| long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId); |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler, |
| routerRecord.mUserRecord.mHandler, |
| uniqueRequestId, managerRequestId, routerRecord, oldSession, route, |
| sessionHints)); |
| } |
| |
| private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| final IBinder binder = router.asBinder(); |
| final RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord == null) { |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "selectRouteWithRouter2 | router: %s(id: %d), route: %s", |
| routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); |
| |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::selectRouteOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route)); |
| } |
| |
| private void deselectRouteWithRouter2Locked(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| final IBinder binder = router.asBinder(); |
| final RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord == null) { |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "deselectRouteWithRouter2 | router: %s(id: %d), route: %s", |
| routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); |
| |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::deselectRouteOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route)); |
| } |
| |
| private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| final IBinder binder = router.asBinder(); |
| final RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord == null) { |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "transferToRouteWithRouter2 | router: %s(id: %d), route: %s", |
| routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); |
| |
| String defaultRouteId = |
| routerRecord.mUserRecord.mHandler.mSystemProvider.getDefaultRoute().getId(); |
| if (route.isSystemRoute() |
| && !routerRecord.hasSystemRoutingPermission() |
| && !TextUtils.equals(route.getId(), defaultRouteId)) { |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::notifySessionCreationFailedToRouter, |
| routerRecord.mUserRecord.mHandler, |
| routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID))); |
| } else { |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::transferToRouteOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route)); |
| } |
| } |
| |
| private void setSessionVolumeWithRouter2Locked(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId, int volume) { |
| final IBinder binder = router.asBinder(); |
| RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord == null) { |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "setSessionVolumeWithRouter2 | router: %s(id: %d), session: %s, volume: %d", |
| routerRecord.mPackageName, |
| routerRecord.mRouterId, |
| uniqueSessionId, |
| volume)); |
| |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::setSessionVolumeOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| DUMMY_REQUEST_ID, uniqueSessionId, volume)); |
| } |
| |
| private void releaseSessionWithRouter2Locked(@NonNull IMediaRouter2 router, |
| @NonNull String uniqueSessionId) { |
| final IBinder binder = router.asBinder(); |
| final RouterRecord routerRecord = mAllRouterRecords.get(binder); |
| |
| if (routerRecord == null) { |
| return; |
| } |
| |
| Slog.i( |
| TAG, |
| TextUtils.formatSimple( |
| "releaseSessionWithRouter2 | router: %s(id: %d), session: %s", |
| routerRecord.mPackageName, routerRecord.mRouterId, uniqueSessionId)); |
| |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::releaseSessionOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| DUMMY_REQUEST_ID, routerRecord, uniqueSessionId)); |
| } |
| |
| // End of locked methods that are used by MediaRouter2. |
| |
| // Start of locked methods that are used by MediaRouter2Manager. |
| |
| private List<RoutingSessionInfo> getRemoteSessionsLocked( |
| @NonNull IMediaRouter2Manager manager) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| Slog.w(TAG, "getRemoteSessionLocked: Ignoring unknown manager"); |
| return Collections.emptyList(); |
| } |
| |
| List<RoutingSessionInfo> sessionInfos = new ArrayList<>(); |
| for (MediaRoute2Provider provider : managerRecord.mUserRecord.mHandler.mRouteProviders) { |
| if (!provider.mIsSystemRouteProvider) { |
| sessionInfos.addAll(provider.getSessionInfos()); |
| } |
| } |
| return sessionInfos; |
| } |
| |
| @GuardedBy("mLock") |
| private void registerManagerLocked(@NonNull IMediaRouter2Manager manager, |
| int uid, int pid, @NonNull String packageName, int userId) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord != null) { |
| Slog.w(TAG, "registerManagerLocked: Same manager already exists. packageName=" |
| + packageName); |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "registerManager | uid: %d, pid: %d, package: %s, user: %d", |
| uid, pid, packageName, userId)); |
| |
| mContext.enforcePermission(Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid, |
| "Must hold MEDIA_CONTENT_CONTROL permission."); |
| |
| UserRecord userRecord = getOrCreateUserRecordLocked(userId); |
| managerRecord = new ManagerRecord(userRecord, manager, uid, pid, packageName); |
| try { |
| binder.linkToDeath(managerRecord, 0); |
| } catch (RemoteException ex) { |
| throw new RuntimeException("Media router manager died prematurely.", ex); |
| } |
| |
| userRecord.mManagerRecords.add(managerRecord); |
| mAllManagerRecords.put(binder, managerRecord); |
| |
| // Note: Features should be sent first before the routes. If not, the |
| // RouteCallback#onRoutesAdded() for system MR2 will never be called with initial routes |
| // due to the lack of features. |
| for (RouterRecord routerRecord : userRecord.mRouterRecords) { |
| // Send route listing preferences before discovery preferences and routes to avoid an |
| // inconsistent state where there are routes to show, but the manager thinks |
| // the app has not expressed a preference for listing. |
| userRecord.mHandler.sendMessage( |
| obtainMessage( |
| UserHandler::notifyRouteListingPreferenceChangeToManagers, |
| routerRecord.mUserRecord.mHandler, |
| routerRecord.mPackageName, |
| routerRecord.mRouteListingPreference)); |
| // TODO: UserRecord <-> routerRecord, why do they reference each other? |
| // How about removing mUserRecord from routerRecord? |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::notifyDiscoveryPreferenceChangedToManager, |
| routerRecord.mUserRecord.mHandler, routerRecord, manager)); |
| } |
| |
| userRecord.mHandler.sendMessage( |
| obtainMessage( |
| UserHandler::notifyInitialRoutesToManager, userRecord.mHandler, manager)); |
| } |
| |
| private void unregisterManagerLocked(@NonNull IMediaRouter2Manager manager, boolean died) { |
| ManagerRecord managerRecord = mAllManagerRecords.remove(manager.asBinder()); |
| if (managerRecord == null) { |
| return; |
| } |
| UserRecord userRecord = managerRecord.mUserRecord; |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "unregisterManager | package: %s, user: %d, manager: %d", |
| managerRecord.mPackageName, |
| userRecord.mUserId, |
| managerRecord.mManagerId)); |
| |
| userRecord.mManagerRecords.remove(managerRecord); |
| managerRecord.dispose(); |
| disposeUserIfNeededLocked(userRecord); // since manager removed from user |
| } |
| |
| private void startScanLocked(@NonNull IMediaRouter2Manager manager) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "startScan | manager: %d", managerRecord.mManagerId)); |
| |
| managerRecord.startScan(); |
| } |
| |
| private void stopScanLocked(@NonNull IMediaRouter2Manager manager) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "stopScan | manager: %d", managerRecord.mManagerId)); |
| |
| managerRecord.stopScan(); |
| } |
| |
| private void setRouteVolumeWithManagerLocked(int requestId, |
| @NonNull IMediaRouter2Manager manager, |
| @NonNull MediaRoute2Info route, int volume) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "setRouteVolumeWithManager | manager: %d, route: %s, volume: %d", |
| managerRecord.mManagerId, route.getId(), volume)); |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| managerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::setRouteVolumeOnHandler, |
| managerRecord.mUserRecord.mHandler, |
| uniqueRequestId, route, volume)); |
| } |
| |
| private void requestCreateSessionWithManagerLocked(int requestId, |
| @NonNull IMediaRouter2Manager manager, @NonNull RoutingSessionInfo oldSession, |
| @NonNull MediaRoute2Info route) { |
| ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder()); |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "requestCreateSessionWithManager | manager: %d, route: %s", |
| managerRecord.mManagerId, route.getId())); |
| |
| String packageName = oldSession.getClientPackageName(); |
| |
| RouterRecord routerRecord = managerRecord.mUserRecord.findRouterRecordLocked(packageName); |
| if (routerRecord == null) { |
| Slog.w(TAG, "requestCreateSessionWithManagerLocked: Ignoring session creation for " |
| + "unknown router."); |
| try { |
| managerRecord.mManager.notifyRequestFailed(requestId, REASON_UNKNOWN_ERROR); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "requestCreateSessionWithManagerLocked: Failed to notify failure. " |
| + "Manager probably died."); |
| } |
| return; |
| } |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| if (managerRecord.mLastSessionCreationRequest != null) { |
| managerRecord.mUserRecord.mHandler.notifyRequestFailedToManager( |
| managerRecord.mManager, |
| toOriginalRequestId(managerRecord.mLastSessionCreationRequest |
| .mManagerRequestId), |
| REASON_UNKNOWN_ERROR); |
| managerRecord.mLastSessionCreationRequest = null; |
| } |
| managerRecord.mLastSessionCreationRequest = new SessionCreationRequest(routerRecord, |
| MediaRoute2ProviderService.REQUEST_ID_NONE, uniqueRequestId, |
| oldSession, route); |
| |
| // Before requesting to the provider, get session hints from the media router. |
| // As a return, media router will request to create a session. |
| routerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::requestRouterCreateSessionOnHandler, |
| routerRecord.mUserRecord.mHandler, |
| uniqueRequestId, routerRecord, managerRecord, oldSession, route)); |
| } |
| |
| private void selectRouteWithManagerLocked(int requestId, @NonNull IMediaRouter2Manager manager, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "selectRouteWithManager | manager: %d, session: %s, route: %s", |
| managerRecord.mManagerId, uniqueSessionId, route.getId())); |
| |
| // Can be null if the session is system's or RCN. |
| RouterRecord routerRecord = managerRecord.mUserRecord.mHandler |
| .findRouterWithSessionLocked(uniqueSessionId); |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| managerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::selectRouteOnHandler, |
| managerRecord.mUserRecord.mHandler, |
| uniqueRequestId, routerRecord, uniqueSessionId, route)); |
| } |
| |
| private void deselectRouteWithManagerLocked(int requestId, |
| @NonNull IMediaRouter2Manager manager, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "deselectRouteWithManager | manager: %d, session: %s, route: %s", |
| managerRecord.mManagerId, uniqueSessionId, route.getId())); |
| |
| // Can be null if the session is system's or RCN. |
| RouterRecord routerRecord = managerRecord.mUserRecord.mHandler |
| .findRouterWithSessionLocked(uniqueSessionId); |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| managerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::deselectRouteOnHandler, |
| managerRecord.mUserRecord.mHandler, |
| uniqueRequestId, routerRecord, uniqueSessionId, route)); |
| } |
| |
| private void transferToRouteWithManagerLocked(int requestId, |
| @NonNull IMediaRouter2Manager manager, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "transferToRouteWithManager | manager: %d, session: %s, route: %s", |
| managerRecord.mManagerId, uniqueSessionId, route.getId())); |
| |
| // Can be null if the session is system's or RCN. |
| RouterRecord routerRecord = managerRecord.mUserRecord.mHandler |
| .findRouterWithSessionLocked(uniqueSessionId); |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| managerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::transferToRouteOnHandler, |
| managerRecord.mUserRecord.mHandler, |
| uniqueRequestId, routerRecord, uniqueSessionId, route)); |
| } |
| |
| private void setSessionVolumeWithManagerLocked(int requestId, |
| @NonNull IMediaRouter2Manager manager, |
| @NonNull String uniqueSessionId, int volume) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "setSessionVolumeWithManager | manager: %d, session: %s, volume: %d", |
| managerRecord.mManagerId, uniqueSessionId, volume)); |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| managerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::setSessionVolumeOnHandler, |
| managerRecord.mUserRecord.mHandler, |
| uniqueRequestId, uniqueSessionId, volume)); |
| } |
| |
| private void releaseSessionWithManagerLocked(int requestId, |
| @NonNull IMediaRouter2Manager manager, @NonNull String uniqueSessionId) { |
| final IBinder binder = manager.asBinder(); |
| ManagerRecord managerRecord = mAllManagerRecords.get(binder); |
| |
| if (managerRecord == null) { |
| return; |
| } |
| |
| Slog.i(TAG, TextUtils.formatSimple( |
| "releaseSessionWithManager | manager: %d, session: %s", |
| managerRecord.mManagerId, uniqueSessionId)); |
| |
| RouterRecord routerRecord = managerRecord.mUserRecord.mHandler |
| .findRouterWithSessionLocked(uniqueSessionId); |
| |
| long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); |
| managerRecord.mUserRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::releaseSessionOnHandler, |
| managerRecord.mUserRecord.mHandler, |
| uniqueRequestId, routerRecord, uniqueSessionId)); |
| } |
| |
| // End of locked methods that are used by MediaRouter2Manager. |
| |
| // Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. |
| |
| @GuardedBy("mLock") |
| private UserRecord getOrCreateUserRecordLocked(int userId) { |
| UserRecord userRecord = mUserRecords.get(userId); |
| if (userRecord == null) { |
| userRecord = new UserRecord(userId); |
| mUserRecords.put(userId, userRecord); |
| userRecord.init(); |
| if (isUserActiveLocked(userId)) { |
| userRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::start, userRecord.mHandler)); |
| } |
| } |
| return userRecord; |
| } |
| |
| @GuardedBy("mLock") |
| private void disposeUserIfNeededLocked(@NonNull UserRecord userRecord) { |
| // If there are no records left and the user is no longer current then go ahead |
| // and purge the user record and all of its associated state. If the user is current |
| // then leave it alone since we might be connected to a route or want to query |
| // the same route information again soon. |
| if (!isUserActiveLocked(userRecord.mUserId) |
| && userRecord.mRouterRecords.isEmpty() |
| && userRecord.mManagerRecords.isEmpty()) { |
| if (DEBUG) { |
| Slog.d(TAG, userRecord + ": Disposed"); |
| } |
| userRecord.mHandler.sendMessage( |
| obtainMessage(UserHandler::stop, userRecord.mHandler)); |
| mUserRecords.remove(userRecord.mUserId); |
| // Note: User already stopped (by switchUser) so no need to send stop message here. |
| } |
| } |
| |
| // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. |
| |
| private void onDeviceConfigChange(@NonNull DeviceConfig.Properties properties) { |
| sPackageImportanceForScanning = properties.getInt( |
| /* name */ KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE, |
| /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE); |
| } |
| |
| static long toUniqueRequestId(int requesterId, int originalRequestId) { |
| return ((long) requesterId << 32) | originalRequestId; |
| } |
| |
| static int toRequesterId(long uniqueRequestId) { |
| return (int) (uniqueRequestId >> 32); |
| } |
| |
| static int toOriginalRequestId(long uniqueRequestId) { |
| return (int) uniqueRequestId; |
| } |
| |
| final class UserRecord { |
| public final int mUserId; |
| //TODO: make records private for thread-safety |
| final ArrayList<RouterRecord> mRouterRecords = new ArrayList<>(); |
| final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>(); |
| RouteDiscoveryPreference mCompositeDiscoveryPreference = RouteDiscoveryPreference.EMPTY; |
| final UserHandler mHandler; |
| |
| UserRecord(int userId) { |
| mUserId = userId; |
| mHandler = new UserHandler(MediaRouter2ServiceImpl.this, this); |
| } |
| |
| void init() { |
| mHandler.init(); |
| } |
| |
| // TODO: This assumes that only one router exists in a package. |
| // Do this in Android S or later. |
| RouterRecord findRouterRecordLocked(String packageName) { |
| for (RouterRecord routerRecord : mRouterRecords) { |
| if (TextUtils.equals(routerRecord.mPackageName, packageName)) { |
| return routerRecord; |
| } |
| } |
| return null; |
| } |
| |
| public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { |
| pw.println(prefix + "UserRecord"); |
| |
| String indent = prefix + " "; |
| |
| pw.println(indent + "mUserId=" + mUserId); |
| |
| pw.println(indent + "Router Records:"); |
| if (!mRouterRecords.isEmpty()) { |
| for (RouterRecord routerRecord : mRouterRecords) { |
| routerRecord.dump(pw, indent + " "); |
| } |
| } else { |
| pw.println(indent + "<no router records>"); |
| } |
| |
| pw.println(indent + "Manager Records:"); |
| if (!mManagerRecords.isEmpty()) { |
| for (ManagerRecord managerRecord : mManagerRecords) { |
| managerRecord.dump(pw, indent + " "); |
| } |
| } else { |
| pw.println(indent + "<no manager records>"); |
| } |
| |
| mCompositeDiscoveryPreference.dump(pw, indent); |
| |
| if (!mHandler.runWithScissors(() -> mHandler.dump(pw, indent), 1000)) { |
| pw.println(indent + "<could not dump handler state>"); |
| } |
| } |
| } |
| |
| final class RouterRecord implements IBinder.DeathRecipient { |
| public final UserRecord mUserRecord; |
| public final String mPackageName; |
| public final List<Integer> mSelectRouteSequenceNumbers; |
| public final IMediaRouter2 mRouter; |
| public final int mUid; |
| public final int mPid; |
| public final boolean mHasConfigureWifiDisplayPermission; |
| public final boolean mHasModifyAudioRoutingPermission; |
| public final AtomicBoolean mHasBluetoothRoutingPermission; |
| public final int mRouterId; |
| |
| public RouteDiscoveryPreference mDiscoveryPreference; |
| @Nullable public RouteListingPreference mRouteListingPreference; |
| |
| RouterRecord(UserRecord userRecord, IMediaRouter2 router, int uid, int pid, |
| String packageName, boolean hasConfigureWifiDisplayPermission, |
| boolean hasModifyAudioRoutingPermission) { |
| mUserRecord = userRecord; |
| mPackageName = packageName; |
| mSelectRouteSequenceNumbers = new ArrayList<>(); |
| mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; |
| mRouter = router; |
| mUid = uid; |
| mPid = pid; |
| mHasConfigureWifiDisplayPermission = hasConfigureWifiDisplayPermission; |
| mHasModifyAudioRoutingPermission = hasModifyAudioRoutingPermission; |
| mHasBluetoothRoutingPermission = new AtomicBoolean(fetchBluetoothPermission()); |
| mRouterId = mNextRouterOrManagerId.getAndIncrement(); |
| } |
| |
| private boolean fetchBluetoothPermission() { |
| boolean hasBluetoothRoutingPermission = true; |
| for (String permission : BLUETOOTH_PERMISSIONS_FOR_SYSTEM_ROUTING) { |
| hasBluetoothRoutingPermission &= |
| mContext.checkPermission(permission, mPid, mUid) |
| == PackageManager.PERMISSION_GRANTED; |
| } |
| return hasBluetoothRoutingPermission; |
| } |
| |
| /** |
| * Returns whether the corresponding router has permission to query and control system |
| * routes. |
| */ |
| public boolean hasSystemRoutingPermission() { |
| return mHasModifyAudioRoutingPermission || mHasBluetoothRoutingPermission.get(); |
| } |
| |
| public void maybeUpdateSystemRoutingPermissionLocked() { |
| boolean oldSystemRoutingPermissionValue = hasSystemRoutingPermission(); |
| mHasBluetoothRoutingPermission.set(fetchBluetoothPermission()); |
| boolean newSystemRoutingPermissionValue = hasSystemRoutingPermission(); |
| if (oldSystemRoutingPermissionValue != newSystemRoutingPermissionValue) { |
| Map<String, MediaRoute2Info> routesToReport = |
| newSystemRoutingPermissionValue |
| ? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters |
| : mUserRecord.mHandler.mLastNotifiedRoutesToNonPrivilegedRouters; |
| notifyRoutesUpdated(routesToReport.values().stream().toList()); |
| |
| List<RoutingSessionInfo> sessionInfos = |
| mUserRecord.mHandler.mSystemProvider.getSessionInfos(); |
| RoutingSessionInfo systemSessionToReport = |
| newSystemRoutingPermissionValue && !sessionInfos.isEmpty() |
| ? sessionInfos.get(0) |
| : mUserRecord.mHandler.mSystemProvider.getDefaultSessionInfo(); |
| notifySessionInfoChanged(systemSessionToReport); |
| } |
| } |
| |
| public void dispose() { |
| mRouter.asBinder().unlinkToDeath(this, 0); |
| } |
| |
| @Override |
| public void binderDied() { |
| routerDied(this); |
| } |
| |
| public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { |
| pw.println(prefix + "RouterRecord"); |
| |
| String indent = prefix + " "; |
| |
| pw.println(indent + "mPackageName=" + mPackageName); |
| pw.println(indent + "mSelectRouteSequenceNumbers=" + mSelectRouteSequenceNumbers); |
| pw.println(indent + "mUid=" + mUid); |
| pw.println(indent + "mPid=" + mPid); |
| pw.println(indent + "mHasConfigureWifiDisplayPermission=" |
| + mHasConfigureWifiDisplayPermission); |
| pw.println( |
| indent |
| + "mHasModifyAudioRoutingPermission=" |
| + mHasModifyAudioRoutingPermission); |
| pw.println( |
| indent |
| + "mHasBluetoothRoutingPermission=" |
| + mHasBluetoothRoutingPermission.get()); |
| pw.println(indent + "hasSystemRoutingPermission=" + hasSystemRoutingPermission()); |
| pw.println(indent + "mRouterId=" + mRouterId); |
| |
| mDiscoveryPreference.dump(pw, indent); |
| } |
| |
| /** |
| * Sends the corresponding router an {@link |
| * android.media.MediaRouter2.RouteCallback#onRoutesUpdated update} for the given {@code |
| * routes}. |
| * |
| * <p>Only the routes that are visible to the router are sent as part of the update. |
| */ |
| public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { |
| try { |
| mRouter.notifyRoutesUpdated(getVisibleRoutes(routes)); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify routes updated. Router probably died.", ex); |
| } |
| } |
| |
| /** |
| * Sends the corresponding router an update for the given session. |
| * |
| * <p>Note: These updates are not directly visible to the app. |
| */ |
| public void notifySessionInfoChanged(RoutingSessionInfo sessionInfo) { |
| try { |
| mRouter.notifySessionInfoChanged(sessionInfo); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); |
| } |
| } |
| |
| /** |
| * Returns a filtered copy of {@code routes} that contains only the routes that are {@link |
| * MediaRoute2Info#isVisibleTo visible} to the router corresponding to this record. |
| */ |
| private List<MediaRoute2Info> getVisibleRoutes(@NonNull List<MediaRoute2Info> routes) { |
| List<MediaRoute2Info> filteredRoutes = new ArrayList<>(); |
| for (MediaRoute2Info route : routes) { |
| if (route.isVisibleTo(mPackageName)) { |
| filteredRoutes.add(route); |
| } |
| } |
| return filteredRoutes; |
| } |
| } |
| |
| final class ManagerRecord implements IBinder.DeathRecipient { |
| public final UserRecord mUserRecord; |
| public final IMediaRouter2Manager mManager; |
| public final int mUid; |
| public final int mPid; |
| public final String mPackageName; |
| public final int mManagerId; |
| public SessionCreationRequest mLastSessionCreationRequest; |
| public boolean mIsScanning; |
| |
| ManagerRecord(UserRecord userRecord, IMediaRouter2Manager manager, |
| int uid, int pid, String packageName) { |
| mUserRecord = userRecord; |
| mManager = manager; |
| mUid = uid; |
| mPid = pid; |
| mPackageName = packageName; |
| mManagerId = mNextRouterOrManagerId.getAndIncrement(); |
| } |
| |
| public void dispose() { |
| mManager.asBinder().unlinkToDeath(this, 0); |
| } |
| |
| @Override |
| public void binderDied() { |
| managerDied(this); |
| } |
| |
| public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { |
| pw.println(prefix + "ManagerRecord"); |
| |
| String indent = prefix + " "; |
| |
| pw.println(indent + "mPackageName=" + mPackageName); |
| pw.println(indent + "mManagerId=" + mManagerId); |
| pw.println(indent + "mUid=" + mUid); |
| pw.println(indent + "mPid=" + mPid); |
| pw.println(indent + "mIsScanning=" + mIsScanning); |
| |
| if (mLastSessionCreationRequest != null) { |
| mLastSessionCreationRequest.dump(pw, indent); |
| } |
| } |
| |
| public void startScan() { |
| if (mIsScanning) { |
| return; |
| } |
| mIsScanning = true; |
| mUserRecord.mHandler.sendMessage(PooledLambda.obtainMessage( |
| UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler)); |
| } |
| |
| public void stopScan() { |
| if (!mIsScanning) { |
| return; |
| } |
| mIsScanning = false; |
| mUserRecord.mHandler.sendMessage(PooledLambda.obtainMessage( |
| UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler)); |
| } |
| |
| @Override |
| public String toString() { |
| return "Manager " + mPackageName + " (pid " + mPid + ")"; |
| } |
| } |
| |
| static final class UserHandler extends Handler implements |
| MediaRoute2ProviderWatcher.Callback, |
| MediaRoute2Provider.Callback { |
| |
| private final WeakReference<MediaRouter2ServiceImpl> mServiceRef; |
| private final UserRecord mUserRecord; |
| private final MediaRoute2ProviderWatcher mWatcher; |
| |
| private final SystemMediaRoute2Provider mSystemProvider; |
| private final ArrayList<MediaRoute2Provider> mRouteProviders = |
| new ArrayList<>(); |
| |
| private final List<MediaRoute2ProviderInfo> mLastProviderInfos = new ArrayList<>(); |
| private final CopyOnWriteArrayList<SessionCreationRequest> mSessionCreationRequests = |
| new CopyOnWriteArrayList<>(); |
| private final Map<String, RouterRecord> mSessionToRouterMap = new ArrayMap<>(); |
| |
| /** |
| * Latest list of routes sent to privileged {@link android.media.MediaRouter2 routers} and |
| * {@link android.media.MediaRouter2Manager managers}. |
| * |
| * <p>Privileged routers are instances of {@link android.media.MediaRouter2 MediaRouter2} |
| * that have {@code MODIFY_AUDIO_ROUTING} permission. |
| * |
| * <p>This list contains all routes exposed by route providers. This includes routes from |
| * both system route providers and user route providers. |
| * |
| * <p>See {@link #getRouterRecords(boolean hasModifyAudioRoutingPermission)}. |
| */ |
| private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToPrivilegedRouters = |
| new ArrayMap<>(); |
| |
| /** |
| * Latest list of routes sent to non-privileged {@link android.media.MediaRouter2 routers}. |
| * |
| * <p>Non-privileged routers are instances of {@link android.media.MediaRouter2 |
| * MediaRouter2} that do <i><b>not</b></i> have {@code MODIFY_AUDIO_ROUTING} permission. |
| * |
| * <p>This list contains all routes exposed by user route providers. It might also include |
| * the current default route from {@link #mSystemProvider} to expose local route updates |
| * (e.g. volume changes) to non-privileged routers. |
| * |
| * <p>See {@link SystemMediaRoute2Provider#mDefaultRoute}. |
| */ |
| private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToNonPrivilegedRouters = |
| new ArrayMap<>(); |
| |
| private boolean mRunning; |
| |
| // TODO: (In Android S+) Pull out SystemMediaRoute2Provider out of UserHandler. |
| UserHandler(@NonNull MediaRouter2ServiceImpl service, @NonNull UserRecord userRecord) { |
| super(Looper.getMainLooper(), null, true); |
| mServiceRef = new WeakReference<>(service); |
| mUserRecord = userRecord; |
| mSystemProvider = new SystemMediaRoute2Provider(service.mContext, |
| UserHandle.of(userRecord.mUserId)); |
| mRouteProviders.add(mSystemProvider); |
| mWatcher = new MediaRoute2ProviderWatcher(service.mContext, this, |
| this, mUserRecord.mUserId); |
| } |
| |
| void init() { |
| mSystemProvider.setCallback(this); |
| } |
| |
| private void start() { |
| if (!mRunning) { |
| mRunning = true; |
| mSystemProvider.start(); |
| mWatcher.start(); |
| } |
| } |
| |
| private void stop() { |
| if (mRunning) { |
| mRunning = false; |
| mWatcher.stop(); // also stops all providers |
| mSystemProvider.stop(); |
| } |
| } |
| |
| @Override |
| public void onAddProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy) { |
| proxy.setCallback(this); |
| mRouteProviders.add(proxy); |
| proxy.updateDiscoveryPreference(mUserRecord.mCompositeDiscoveryPreference); |
| } |
| |
| @Override |
| public void onRemoveProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy) { |
| mRouteProviders.remove(proxy); |
| } |
| |
| @Override |
| public void onProviderStateChanged(@NonNull MediaRoute2Provider provider) { |
| sendMessage(PooledLambda.obtainMessage(UserHandler::onProviderStateChangedOnHandler, |
| this, provider)); |
| } |
| |
| @Override |
| public void onSessionCreated(@NonNull MediaRoute2Provider provider, |
| long uniqueRequestId, @NonNull RoutingSessionInfo sessionInfo) { |
| sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionCreatedOnHandler, |
| this, provider, uniqueRequestId, sessionInfo)); |
| } |
| |
| @Override |
| public void onSessionUpdated(@NonNull MediaRoute2Provider provider, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionInfoChangedOnHandler, |
| this, provider, sessionInfo)); |
| } |
| |
| @Override |
| public void onSessionReleased(@NonNull MediaRoute2Provider provider, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionReleasedOnHandler, |
| this, provider, sessionInfo)); |
| } |
| |
| @Override |
| public void onRequestFailed(@NonNull MediaRoute2Provider provider, long uniqueRequestId, |
| int reason) { |
| sendMessage(PooledLambda.obtainMessage(UserHandler::onRequestFailedOnHandler, |
| this, provider, uniqueRequestId, reason)); |
| } |
| |
| @Nullable |
| public RouterRecord findRouterWithSessionLocked(@NonNull String uniqueSessionId) { |
| return mSessionToRouterMap.get(uniqueSessionId); |
| } |
| |
| @Nullable |
| public ManagerRecord findManagerWithId(int managerId) { |
| for (ManagerRecord manager : getManagerRecords()) { |
| if (manager.mManagerId == managerId) { |
| return manager; |
| } |
| } |
| return null; |
| } |
| |
| public void maybeUpdateDiscoveryPreferenceForUid(int uid) { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return; |
| } |
| boolean isUidRelevant; |
| synchronized (service.mLock) { |
| isUidRelevant = mUserRecord.mRouterRecords.stream().anyMatch( |
| router -> router.mUid == uid) |
| | mUserRecord.mManagerRecords.stream().anyMatch( |
| manager -> manager.mUid == uid); |
| } |
| if (isUidRelevant) { |
| sendMessage(PooledLambda.obtainMessage( |
| UserHandler::updateDiscoveryPreferenceOnHandler, this)); |
| } |
| } |
| |
| public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { |
| pw.println(prefix + "UserHandler"); |
| |
| String indent = prefix + " "; |
| pw.println(indent + "mRunning=" + mRunning); |
| |
| mSystemProvider.dump(pw, prefix); |
| mWatcher.dump(pw, prefix); |
| } |
| |
| private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) { |
| MediaRoute2ProviderInfo newInfo = provider.getProviderInfo(); |
| int providerInfoIndex = |
| indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos); |
| MediaRoute2ProviderInfo oldInfo = |
| providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex); |
| MediaRouter2ServiceImpl mediaRouter2Service = mServiceRef.get(); |
| |
| if (oldInfo == newInfo) { |
| // Nothing to do. |
| return; |
| } |
| |
| Collection<MediaRoute2Info> newRoutes; |
| Set<String> newRouteIds; |
| if (newInfo != null) { |
| // Adding or updating a provider. |
| newRoutes = newInfo.getRoutes(); |
| newRouteIds = |
| newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet()); |
| if (providerInfoIndex >= 0) { |
| mLastProviderInfos.set(providerInfoIndex, newInfo); |
| } else { |
| mLastProviderInfos.add(newInfo); |
| } |
| } else /* newInfo == null */ { |
| // Removing a provider. |
| mLastProviderInfos.remove(oldInfo); |
| newRouteIds = Collections.emptySet(); |
| newRoutes = Collections.emptySet(); |
| } |
| |
| // Add new routes to the maps. |
| ArrayList<MediaRoute2Info> addedRoutes = new ArrayList<>(); |
| boolean hasAddedOrModifiedRoutes = false; |
| for (MediaRoute2Info newRouteInfo : newRoutes) { |
| if (!newRouteInfo.isValid()) { |
| Slog.w(TAG, "onProviderStateChangedOnHandler: Ignoring invalid route : " |
| + newRouteInfo); |
| continue; |
| } |
| if (!provider.mIsSystemRouteProvider) { |
| mLastNotifiedRoutesToNonPrivilegedRouters.put( |
| newRouteInfo.getId(), newRouteInfo); |
| } |
| MediaRoute2Info oldRouteInfo = |
| mLastNotifiedRoutesToPrivilegedRouters.put( |
| newRouteInfo.getId(), newRouteInfo); |
| hasAddedOrModifiedRoutes |= !newRouteInfo.equals(oldRouteInfo); |
| if (oldRouteInfo == null) { |
| addedRoutes.add(newRouteInfo); |
| } |
| } |
| |
| // Remove stale routes from the maps. |
| ArrayList<MediaRoute2Info> removedRoutes = new ArrayList<>(); |
| Collection<MediaRoute2Info> oldRoutes = |
| oldInfo == null ? Collections.emptyList() : oldInfo.getRoutes(); |
| boolean hasRemovedRoutes = false; |
| for (MediaRoute2Info oldRoute : oldRoutes) { |
| String oldRouteId = oldRoute.getId(); |
| if (!newRouteIds.contains(oldRouteId)) { |
| hasRemovedRoutes = true; |
| mLastNotifiedRoutesToPrivilegedRouters.remove(oldRouteId); |
| mLastNotifiedRoutesToNonPrivilegedRouters.remove(oldRouteId); |
| removedRoutes.add(oldRoute); |
| } |
| } |
| |
| if (!addedRoutes.isEmpty()) { |
| // If routes were added, newInfo cannot be null. |
| Slog.i(TAG, |
| toLoggingMessage( |
| /* source= */ "addProviderRoutes", |
| newInfo.getUniqueId(), |
| addedRoutes)); |
| } |
| if (!removedRoutes.isEmpty()) { |
| // If routes were removed, oldInfo cannot be null. |
| Slog.i(TAG, |
| toLoggingMessage( |
| /* source= */ "removeProviderRoutes", |
| oldInfo.getUniqueId(), |
| removedRoutes)); |
| } |
| |
| dispatchUpdates( |
| hasAddedOrModifiedRoutes, |
| hasRemovedRoutes, |
| provider.mIsSystemRouteProvider, |
| mSystemProvider.getDefaultRoute()); |
| } |
| |
| private static String toLoggingMessage( |
| String source, String providerId, ArrayList<MediaRoute2Info> routes) { |
| String routesString = |
| routes.stream() |
| .map(it -> String.format("%s | %s", it.getOriginalId(), it.getName())) |
| .collect(Collectors.joining(/* delimiter= */ ", ")); |
| return TextUtils.formatSimple("%s | provider: %s, routes: [%s]", |
| source, providerId, routesString); |
| } |
| |
| /** |
| * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters} |
| * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link |
| * android.media.MediaRouter2 routers} and {@link MediaRouter2Manager managers} after a call |
| * to {@link #onProviderStateChangedOnHandler(MediaRoute2Provider)}. Ignores if no changes |
| * were made. |
| * |
| * @param hasAddedOrModifiedRoutes whether routes were added or modified. |
| * @param hasRemovedRoutes whether routes were removed. |
| * @param isSystemProvider whether the latest update was caused by a system provider. |
| * @param defaultRoute the current default route in {@link #mSystemProvider}. |
| */ |
| private void dispatchUpdates( |
| boolean hasAddedOrModifiedRoutes, |
| boolean hasRemovedRoutes, |
| boolean isSystemProvider, |
| MediaRoute2Info defaultRoute) { |
| |
| // Ignore if no changes. |
| if (!hasAddedOrModifiedRoutes && !hasRemovedRoutes) { |
| return; |
| } |
| List<RouterRecord> routerRecordsWithModifyAudioRoutingPermission = |
| getRouterRecords(true); |
| List<RouterRecord> routerRecordsWithoutModifyAudioRoutingPermission = |
| getRouterRecords(false); |
| List<IMediaRouter2Manager> managers = getManagers(); |
| |
| // Managers receive all provider updates with all routes. |
| notifyRoutesUpdatedToManagers( |
| managers, new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); |
| |
| // Routers with modify audio permission (usually system routers) receive all provider |
| // updates with all routes. |
| notifyRoutesUpdatedToRouterRecords( |
| routerRecordsWithModifyAudioRoutingPermission, |
| new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); |
| |
| if (!isSystemProvider) { |
| // Regular routers receive updates from all non-system providers with all non-system |
| // routes. |
| notifyRoutesUpdatedToRouterRecords( |
| routerRecordsWithoutModifyAudioRoutingPermission, |
| new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); |
| } else if (hasAddedOrModifiedRoutes) { |
| // On system provider updates, regular routers receive the updated default route. |
| // This is the only system route they should receive. |
| mLastNotifiedRoutesToNonPrivilegedRouters.put(defaultRoute.getId(), defaultRoute); |
| notifyRoutesUpdatedToRouterRecords( |
| routerRecordsWithoutModifyAudioRoutingPermission, |
| new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); |
| } |
| } |
| |
| /** |
| * Returns the index of the first element in {@code lastProviderInfos} that matches the |
| * specified unique id. |
| * |
| * @param uniqueId unique id of {@link MediaRoute2ProviderInfo} to be found. |
| * @param lastProviderInfos list of {@link MediaRoute2ProviderInfo}. |
| * @return index of found element, or -1 if not found. |
| */ |
| private static int indexOfRouteProviderInfoByUniqueId( |
| @NonNull String uniqueId, |
| @NonNull List<MediaRoute2ProviderInfo> lastProviderInfos) { |
| for (int i = 0; i < lastProviderInfos.size(); i++) { |
| MediaRoute2ProviderInfo providerInfo = lastProviderInfos.get(i); |
| if (TextUtils.equals(providerInfo.getUniqueId(), uniqueId)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| private void requestRouterCreateSessionOnHandler(long uniqueRequestId, |
| @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord, |
| @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { |
| try { |
| if (route.isSystemRoute() && !routerRecord.hasSystemRoutingPermission()) { |
| // The router lacks permission to modify system routing, so we hide system |
| // route info from them. |
| route = mSystemProvider.getDefaultRoute(); |
| } |
| routerRecord.mRouter.requestCreateSessionByManager( |
| uniqueRequestId, oldSession, route); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "getSessionHintsForCreatingSessionOnHandler: " |
| + "Failed to request. Router probably died.", ex); |
| notifyRequestFailedToManager(managerRecord.mManager, |
| toOriginalRequestId(uniqueRequestId), REASON_UNKNOWN_ERROR); |
| } |
| } |
| |
| private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId, |
| long managerRequestId, @NonNull RouterRecord routerRecord, |
| @NonNull RoutingSessionInfo oldSession, |
| @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { |
| |
| final MediaRoute2Provider provider = findProvider(route.getProviderId()); |
| if (provider == null) { |
| Slog.w(TAG, "requestCreateSessionWithRouter2OnHandler: Ignoring session " |
| + "creation request since no provider found for given route=" + route); |
| notifySessionCreationFailedToRouter(routerRecord, |
| toOriginalRequestId(uniqueRequestId)); |
| return; |
| } |
| |
| SessionCreationRequest request = |
| new SessionCreationRequest(routerRecord, uniqueRequestId, |
| managerRequestId, oldSession, route); |
| mSessionCreationRequests.add(request); |
| |
| provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName, |
| route.getOriginalId(), sessionHints); |
| } |
| |
| // routerRecord can be null if the session is system's or RCN. |
| private void selectRouteOnHandler(long uniqueRequestId, @Nullable RouterRecord routerRecord, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, |
| "selecting")) { |
| return; |
| } |
| |
| final String providerId = route.getProviderId(); |
| final MediaRoute2Provider provider = findProvider(providerId); |
| if (provider == null) { |
| return; |
| } |
| provider.selectRoute(uniqueRequestId, getOriginalId(uniqueSessionId), |
| route.getOriginalId()); |
| } |
| |
| // routerRecord can be null if the session is system's or RCN. |
| private void deselectRouteOnHandler(long uniqueRequestId, |
| @Nullable RouterRecord routerRecord, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, |
| "deselecting")) { |
| return; |
| } |
| |
| final String providerId = route.getProviderId(); |
| final MediaRoute2Provider provider = findProvider(providerId); |
| if (provider == null) { |
| return; |
| } |
| |
| provider.deselectRoute(uniqueRequestId, getOriginalId(uniqueSessionId), |
| route.getOriginalId()); |
| } |
| |
| // routerRecord can be null if the session is system's or RCN. |
| private void transferToRouteOnHandler(long uniqueRequestId, |
| @Nullable RouterRecord routerRecord, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { |
| if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, |
| "transferring to")) { |
| return; |
| } |
| |
| final String providerId = route.getProviderId(); |
| final MediaRoute2Provider provider = findProvider(providerId); |
| if (provider == null) { |
| return; |
| } |
| provider.transferToRoute(uniqueRequestId, getOriginalId(uniqueSessionId), |
| route.getOriginalId()); |
| } |
| |
| // routerRecord is null if and only if the session is created without the request, which |
| // includes the system's session and RCN cases. |
| private boolean checkArgumentsForSessionControl(@Nullable RouterRecord routerRecord, |
| @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route, |
| @NonNull String description) { |
| final String providerId = route.getProviderId(); |
| final MediaRoute2Provider provider = findProvider(providerId); |
| if (provider == null) { |
| Slog.w(TAG, "Ignoring " + description + " route since no provider found for " |
| + "given route=" + route); |
| return false; |
| } |
| |
| // Bypass checking router if it's the system session (routerRecord should be null) |
| if (TextUtils.equals(getProviderId(uniqueSessionId), mSystemProvider.getUniqueId())) { |
| return true; |
| } |
| |
| RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId); |
| if (matchingRecord != routerRecord) { |
| Slog.w(TAG, "Ignoring " + description + " route from non-matching router. " |
| + "packageName=" + routerRecord.mPackageName + " route=" + route); |
| return false; |
| } |
| |
| final String sessionId = getOriginalId(uniqueSessionId); |
| if (sessionId == null) { |
| Slog.w(TAG, "Failed to get original session id from unique session id. " |
| + "uniqueSessionId=" + uniqueSessionId); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private void setRouteVolumeOnHandler(long uniqueRequestId, @NonNull MediaRoute2Info route, |
| int volume) { |
| final MediaRoute2Provider provider = findProvider(route.getProviderId()); |
| if (provider == null) { |
| Slog.w(TAG, "setRouteVolumeOnHandler: Couldn't find provider for route=" + route); |
| return; |
| } |
| provider.setRouteVolume(uniqueRequestId, route.getOriginalId(), volume); |
| } |
| |
| private void setSessionVolumeOnHandler(long uniqueRequestId, |
| @NonNull String uniqueSessionId, int volume) { |
| final MediaRoute2Provider provider = findProvider(getProviderId(uniqueSessionId)); |
| if (provider == null) { |
| Slog.w(TAG, "setSessionVolumeOnHandler: Couldn't find provider for session id=" |
| + uniqueSessionId); |
| return; |
| } |
| provider.setSessionVolume(uniqueRequestId, getOriginalId(uniqueSessionId), volume); |
| } |
| |
| private void releaseSessionOnHandler(long uniqueRequestId, |
| @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId) { |
| final RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId); |
| if (matchingRecord != routerRecord) { |
| Slog.w(TAG, "Ignoring releasing session from non-matching router. packageName=" |
| + (routerRecord == null ? null : routerRecord.mPackageName) |
| + " uniqueSessionId=" + uniqueSessionId); |
| return; |
| } |
| |
| final String providerId = getProviderId(uniqueSessionId); |
| if (providerId == null) { |
| Slog.w(TAG, "Ignoring releasing session with invalid unique session ID. " |
| + "uniqueSessionId=" + uniqueSessionId); |
| return; |
| } |
| |
| final String sessionId = getOriginalId(uniqueSessionId); |
| if (sessionId == null) { |
| Slog.w(TAG, "Ignoring releasing session with invalid unique session ID. " |
| + "uniqueSessionId=" + uniqueSessionId + " providerId=" + providerId); |
| return; |
| } |
| |
| final MediaRoute2Provider provider = findProvider(providerId); |
| if (provider == null) { |
| Slog.w(TAG, "Ignoring releasing session since no provider found for given " |
| + "providerId=" + providerId); |
| return; |
| } |
| |
| provider.releaseSession(uniqueRequestId, sessionId); |
| } |
| |
| private void onSessionCreatedOnHandler(@NonNull MediaRoute2Provider provider, |
| long uniqueRequestId, @NonNull RoutingSessionInfo sessionInfo) { |
| SessionCreationRequest matchingRequest = null; |
| |
| for (SessionCreationRequest request : mSessionCreationRequests) { |
| if (request.mUniqueRequestId == uniqueRequestId |
| && TextUtils.equals( |
| request.mRoute.getProviderId(), provider.getUniqueId())) { |
| matchingRequest = request; |
| break; |
| } |
| } |
| |
| long managerRequestId = (matchingRequest == null) |
| ? MediaRoute2ProviderService.REQUEST_ID_NONE |
| : matchingRequest.mManagerRequestId; |
| notifySessionCreatedToManagers(managerRequestId, sessionInfo); |
| |
| if (matchingRequest == null) { |
| Slog.w(TAG, "Ignoring session creation result for unknown request. " |
| + "uniqueRequestId=" + uniqueRequestId + ", sessionInfo=" + sessionInfo); |
| return; |
| } |
| |
| mSessionCreationRequests.remove(matchingRequest); |
| // Not to show old session |
| MediaRoute2Provider oldProvider = |
| findProvider(matchingRequest.mOldSession.getProviderId()); |
| if (oldProvider != null) { |
| oldProvider.prepareReleaseSession(matchingRequest.mOldSession.getId()); |
| } else { |
| Slog.w(TAG, "onSessionCreatedOnHandler: Can't find provider for an old session. " |
| + "session=" + matchingRequest.mOldSession); |
| } |
| |
| mSessionToRouterMap.put(sessionInfo.getId(), matchingRequest.mRouterRecord); |
| if (sessionInfo.isSystemSession() |
| && !matchingRequest.mRouterRecord.hasSystemRoutingPermission()) { |
| // The router lacks permission to modify system routing, so we hide system routing |
| // session info from them. |
| sessionInfo = mSystemProvider.getDefaultSessionInfo(); |
| } |
| notifySessionCreatedToRouter( |
| matchingRequest.mRouterRecord, |
| toOriginalRequestId(uniqueRequestId), |
| sessionInfo); |
| } |
| |
| private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| List<IMediaRouter2Manager> managers = getManagers(); |
| notifySessionUpdatedToManagers(managers, sessionInfo); |
| |
| // For system provider, notify all routers. |
| if (provider == mSystemProvider) { |
| if (mServiceRef.get() == null) { |
| return; |
| } |
| notifySessionInfoChangedToRouters(getRouterRecords(true), sessionInfo); |
| notifySessionInfoChangedToRouters(getRouterRecords(false), |
| mSystemProvider.getDefaultSessionInfo()); |
| return; |
| } |
| |
| RouterRecord routerRecord = mSessionToRouterMap.get(sessionInfo.getId()); |
| if (routerRecord == null) { |
| Slog.w(TAG, "onSessionInfoChangedOnHandler: No matching router found for session=" |
| + sessionInfo); |
| return; |
| } |
| notifySessionInfoChangedToRouters(Arrays.asList(routerRecord), sessionInfo); |
| } |
| |
| private void onSessionReleasedOnHandler(@NonNull MediaRoute2Provider provider, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| List<IMediaRouter2Manager> managers = getManagers(); |
| notifySessionReleasedToManagers(managers, sessionInfo); |
| |
| RouterRecord routerRecord = mSessionToRouterMap.get(sessionInfo.getId()); |
| if (routerRecord == null) { |
| Slog.w(TAG, "onSessionReleasedOnHandler: No matching router found for session=" |
| + sessionInfo); |
| return; |
| } |
| notifySessionReleasedToRouter(routerRecord, sessionInfo); |
| } |
| |
| private void onRequestFailedOnHandler(@NonNull MediaRoute2Provider provider, |
| long uniqueRequestId, int reason) { |
| if (handleSessionCreationRequestFailed(provider, uniqueRequestId, reason)) { |
| return; |
| } |
| |
| final int requesterId = toRequesterId(uniqueRequestId); |
| ManagerRecord manager = findManagerWithId(requesterId); |
| if (manager != null) { |
| notifyRequestFailedToManager( |
| manager.mManager, toOriginalRequestId(uniqueRequestId), reason); |
| return; |
| } |
| |
| // Currently, only the manager can get notified of failures. |
| // TODO: Notify router too when the related callback is introduced. |
| } |
| |
| private boolean handleSessionCreationRequestFailed(@NonNull MediaRoute2Provider provider, |
| long uniqueRequestId, int reason) { |
| // Check whether the failure is about creating a session |
| SessionCreationRequest matchingRequest = null; |
| for (SessionCreationRequest request : mSessionCreationRequests) { |
| if (request.mUniqueRequestId == uniqueRequestId && TextUtils.equals( |
| request.mRoute.getProviderId(), provider.getUniqueId())) { |
| matchingRequest = request; |
| break; |
| } |
| } |
| |
| if (matchingRequest == null) { |
| // The failure is not about creating a session. |
| return false; |
| } |
| |
| mSessionCreationRequests.remove(matchingRequest); |
| |
| // Notify the requester about the failure. |
| // The call should be made by either MediaRouter2 or MediaRouter2Manager. |
| if (matchingRequest.mManagerRequestId == MediaRouter2Manager.REQUEST_ID_NONE) { |
| notifySessionCreationFailedToRouter( |
| matchingRequest.mRouterRecord, toOriginalRequestId(uniqueRequestId)); |
| } else { |
| final int requesterId = toRequesterId(matchingRequest.mManagerRequestId); |
| ManagerRecord manager = findManagerWithId(requesterId); |
| if (manager != null) { |
| notifyRequestFailedToManager(manager.mManager, |
| toOriginalRequestId(matchingRequest.mManagerRequestId), reason); |
| } |
| } |
| return true; |
| } |
| |
| private void notifySessionCreatedToRouter(@NonNull RouterRecord routerRecord, |
| int requestId, @NonNull RoutingSessionInfo sessionInfo) { |
| try { |
| routerRecord.mRouter.notifySessionCreated(requestId, sessionInfo); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify router of the session creation." |
| + " Router probably died.", ex); |
| } |
| } |
| |
| private void notifySessionCreationFailedToRouter(@NonNull RouterRecord routerRecord, |
| int requestId) { |
| try { |
| routerRecord.mRouter.notifySessionCreated(requestId, |
| /* sessionInfo= */ null); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify router of the session creation failure." |
| + " Router probably died.", ex); |
| } |
| } |
| |
| private void notifySessionReleasedToRouter(@NonNull RouterRecord routerRecord, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| try { |
| routerRecord.mRouter.notifySessionReleased(sessionInfo); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify router of the session release." |
| + " Router probably died.", ex); |
| } |
| } |
| |
| private List<IMediaRouter2Manager> getManagers() { |
| final List<IMediaRouter2Manager> managers = new ArrayList<>(); |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return managers; |
| } |
| synchronized (service.mLock) { |
| for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) { |
| managers.add(managerRecord.mManager); |
| } |
| } |
| return managers; |
| } |
| |
| private List<RouterRecord> getRouterRecords() { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return Collections.emptyList(); |
| } |
| synchronized (service.mLock) { |
| return new ArrayList<>(mUserRecord.mRouterRecords); |
| } |
| } |
| |
| private List<RouterRecord> getRouterRecords(boolean hasModifyAudioRoutingPermission) { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| List<RouterRecord> routerRecords = new ArrayList<>(); |
| if (service == null) { |
| return routerRecords; |
| } |
| synchronized (service.mLock) { |
| for (RouterRecord routerRecord : mUserRecord.mRouterRecords) { |
| if (hasModifyAudioRoutingPermission |
| == routerRecord.hasSystemRoutingPermission()) { |
| routerRecords.add(routerRecord); |
| } |
| } |
| return routerRecords; |
| } |
| } |
| |
| private List<ManagerRecord> getManagerRecords() { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return Collections.emptyList(); |
| } |
| synchronized (service.mLock) { |
| return new ArrayList<>(mUserRecord.mManagerRecords); |
| } |
| } |
| |
| private void notifyRouterRegistered(@NonNull RouterRecord routerRecord) { |
| List<MediaRoute2Info> currentRoutes = new ArrayList<>(); |
| |
| MediaRoute2ProviderInfo systemProviderInfo = null; |
| for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { |
| // TODO: Create MediaRoute2ProviderInfo#isSystemProvider() |
| if (TextUtils.equals(providerInfo.getUniqueId(), mSystemProvider.getUniqueId())) { |
| // Adding routes from system provider will be handled below, so skip it here. |
| systemProviderInfo = providerInfo; |
| continue; |
| } |
| currentRoutes.addAll(providerInfo.getRoutes()); |
| } |
| |
| RoutingSessionInfo currentSystemSessionInfo; |
| if (routerRecord.hasSystemRoutingPermission()) { |
| if (systemProviderInfo != null) { |
| currentRoutes.addAll(systemProviderInfo.getRoutes()); |
| } else { |
| // This shouldn't happen. |
| Slog.wtf(TAG, "System route provider not found."); |
| } |
| currentSystemSessionInfo = mSystemProvider.getSessionInfos().get(0); |
| } else { |
| currentRoutes.add(mSystemProvider.getDefaultRoute()); |
| currentSystemSessionInfo = mSystemProvider.getDefaultSessionInfo(); |
| } |
| |
| if (currentRoutes.size() == 0) { |
| return; |
| } |
| |
| try { |
| routerRecord.mRouter.notifyRouterRegistered( |
| currentRoutes, currentSystemSessionInfo); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify router registered. Router probably died.", ex); |
| } |
| } |
| |
| private static void notifyRoutesUpdatedToRouterRecords( |
| @NonNull List<RouterRecord> routerRecords, |
| @NonNull List<MediaRoute2Info> routes) { |
| for (RouterRecord routerRecord : routerRecords) { |
| routerRecord.notifyRoutesUpdated(routes); |
| } |
| } |
| |
| private void notifySessionInfoChangedToRouters( |
| @NonNull List<RouterRecord> routerRecords, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| for (RouterRecord routerRecord : routerRecords) { |
| routerRecord.notifySessionInfoChanged(sessionInfo); |
| } |
| } |
| |
| /** |
| * Notifies {@code manager} with all known routes. This only happens once after {@code |
| * manager} is registered through {@link #registerManager(IMediaRouter2Manager, String) |
| * registerManager()}. |
| * |
| * @param manager {@link IMediaRouter2Manager} to be notified. |
| */ |
| private void notifyInitialRoutesToManager(@NonNull IMediaRouter2Manager manager) { |
| if (mLastNotifiedRoutesToPrivilegedRouters.isEmpty()) { |
| return; |
| } |
| try { |
| manager.notifyRoutesUpdated( |
| new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify all routes. Manager probably died.", ex); |
| } |
| } |
| |
| private void notifyRoutesUpdatedToManagers( |
| @NonNull List<IMediaRouter2Manager> managers, |
| @NonNull List<MediaRoute2Info> routes) { |
| for (IMediaRouter2Manager manager : managers) { |
| try { |
| manager.notifyRoutesUpdated(routes); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify routes changed. Manager probably died.", ex); |
| } |
| } |
| } |
| |
| private void notifySessionCreatedToManagers(long managerRequestId, |
| @NonNull RoutingSessionInfo session) { |
| int requesterId = toRequesterId(managerRequestId); |
| int originalRequestId = toOriginalRequestId(managerRequestId); |
| |
| for (ManagerRecord manager : getManagerRecords()) { |
| try { |
| manager.mManager.notifySessionCreated( |
| ((manager.mManagerId == requesterId) ? originalRequestId : |
| MediaRouter2Manager.REQUEST_ID_NONE), session); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "notifySessionCreatedToManagers: " |
| + "Failed to notify. Manager probably died.", ex); |
| } |
| } |
| } |
| |
| private void notifySessionUpdatedToManagers( |
| @NonNull List<IMediaRouter2Manager> managers, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| for (IMediaRouter2Manager manager : managers) { |
| try { |
| manager.notifySessionUpdated(sessionInfo); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "notifySessionUpdatedToManagers: " |
| + "Failed to notify. Manager probably died.", ex); |
| } |
| } |
| } |
| |
| private void notifySessionReleasedToManagers( |
| @NonNull List<IMediaRouter2Manager> managers, |
| @NonNull RoutingSessionInfo sessionInfo) { |
| for (IMediaRouter2Manager manager : managers) { |
| try { |
| manager.notifySessionReleased(sessionInfo); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "notifySessionReleasedToManagers: " |
| + "Failed to notify. Manager probably died.", ex); |
| } |
| } |
| } |
| |
| private void notifyDiscoveryPreferenceChangedToManager(@NonNull RouterRecord routerRecord, |
| @NonNull IMediaRouter2Manager manager) { |
| try { |
| manager.notifyDiscoveryPreferenceChanged(routerRecord.mPackageName, |
| routerRecord.mDiscoveryPreference); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify preferred features changed." |
| + " Manager probably died.", ex); |
| } |
| } |
| |
| private void notifyDiscoveryPreferenceChangedToManagers(@NonNull String routerPackageName, |
| @Nullable RouteDiscoveryPreference discoveryPreference) { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return; |
| } |
| List<IMediaRouter2Manager> managers = new ArrayList<>(); |
| synchronized (service.mLock) { |
| for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) { |
| managers.add(managerRecord.mManager); |
| } |
| } |
| for (IMediaRouter2Manager manager : managers) { |
| try { |
| manager.notifyDiscoveryPreferenceChanged(routerPackageName, |
| discoveryPreference); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify preferred features changed." |
| + " Manager probably died.", ex); |
| } |
| } |
| } |
| |
| private void notifyRouteListingPreferenceChangeToManagers( |
| String routerPackageName, @Nullable RouteListingPreference routeListingPreference) { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return; |
| } |
| List<IMediaRouter2Manager> managers = new ArrayList<>(); |
| synchronized (service.mLock) { |
| for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) { |
| managers.add(managerRecord.mManager); |
| } |
| } |
| for (IMediaRouter2Manager manager : managers) { |
| try { |
| manager.notifyRouteListingPreferenceChange( |
| routerPackageName, routeListingPreference); |
| } catch (RemoteException ex) { |
| Slog.w( |
| TAG, |
| "Failed to notify preferred features changed." |
| + " Manager probably died.", |
| ex); |
| } |
| } |
| // TODO(b/238178508): In order to support privileged media router instances, we also |
| // need to update routers other than the one making the update. |
| } |
| |
| private void notifyRequestFailedToManager(@NonNull IMediaRouter2Manager manager, |
| int requestId, int reason) { |
| try { |
| manager.notifyRequestFailed(requestId, reason); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify manager of the request failure." |
| + " Manager probably died.", ex); |
| } |
| } |
| |
| private void updateDiscoveryPreferenceOnHandler() { |
| MediaRouter2ServiceImpl service = mServiceRef.get(); |
| if (service == null) { |
| return; |
| } |
| List<RouteDiscoveryPreference> discoveryPreferences = Collections.emptyList(); |
| List<RouterRecord> routerRecords = getRouterRecords(); |
| List<ManagerRecord> managerRecords = getManagerRecords(); |
| |
| boolean isManagerScanning = false; |
| if (service.mPowerManager.isInteractive()) { |
| isManagerScanning = managerRecords.stream().anyMatch(manager -> |
| manager.mIsScanning && service.mActivityManager |
| .getPackageImportance(manager.mPackageName) |
| <= sPackageImportanceForScanning); |
| |
| if (isManagerScanning) { |
| discoveryPreferences = routerRecords.stream() |
| .map(record -> record.mDiscoveryPreference) |
| .collect(Collectors.toList()); |
| } else { |
| discoveryPreferences = routerRecords.stream().filter(record -> |
| service.mActivityManager.getPackageImportance(record.mPackageName) |
| <= sPackageImportanceForScanning) |
| .map(record -> record.mDiscoveryPreference) |
| .collect(Collectors.toList()); |
| } |
| } |
| |
| for (MediaRoute2Provider provider : mRouteProviders) { |
| if (provider instanceof MediaRoute2ProviderServiceProxy) { |
| ((MediaRoute2ProviderServiceProxy) provider) |
| .setManagerScanning(isManagerScanning); |
| } |
| } |
| |
| // Build a composite RouteDiscoveryPreference that matches all of the routes |
| // that match one or more of the individual discovery preferences. It may also |
| // match additional routes. The composite RouteDiscoveryPreference can be used |
| // to query route providers once to obtain all of the routes of interest, which |
| // can be subsequently filtered for the individual discovery preferences. |
| Set<String> preferredFeatures = new HashSet<>(); |
| boolean activeScan = false; |
| for (RouteDiscoveryPreference preference : discoveryPreferences) { |
| preferredFeatures.addAll(preference.getPreferredFeatures()); |
| activeScan |= preference.shouldPerformActiveScan(); |
| } |
| RouteDiscoveryPreference newPreference = new RouteDiscoveryPreference.Builder( |
| List.copyOf(preferredFeatures), activeScan || isManagerScanning).build(); |
| |
| synchronized (service.mLock) { |
| if (newPreference.equals(mUserRecord.mCompositeDiscoveryPreference)) { |
| return; |
| } |
| mUserRecord.mCompositeDiscoveryPreference = newPreference; |
| } |
| for (MediaRoute2Provider provider : mRouteProviders) { |
| provider.updateDiscoveryPreference(mUserRecord.mCompositeDiscoveryPreference); |
| } |
| } |
| |
| private MediaRoute2Provider findProvider(@Nullable String providerId) { |
| for (MediaRoute2Provider provider : mRouteProviders) { |
| if (TextUtils.equals(provider.getUniqueId(), providerId)) { |
| return provider; |
| } |
| } |
| return null; |
| } |
| } |
| static final class SessionCreationRequest { |
| public final RouterRecord mRouterRecord; |
| public final long mUniqueRequestId; |
| public final long mManagerRequestId; |
| public final RoutingSessionInfo mOldSession; |
| public final MediaRoute2Info mRoute; |
| |
| SessionCreationRequest(@NonNull RouterRecord routerRecord, long uniqueRequestId, |
| long managerRequestId, @NonNull RoutingSessionInfo oldSession, |
| @NonNull MediaRoute2Info route) { |
| mRouterRecord = routerRecord; |
| mUniqueRequestId = uniqueRequestId; |
| mManagerRequestId = managerRequestId; |
| mOldSession = oldSession; |
| mRoute = route; |
| } |
| |
| public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { |
| pw.println(prefix + "SessionCreationRequest"); |
| |
| String indent = prefix + " "; |
| |
| pw.println(indent + "mUniqueRequestId=" + mUniqueRequestId); |
| pw.println(indent + "mManagerRequestId=" + mManagerRequestId); |
| mOldSession.dump(pw, indent); |
| mRoute.dump(pw, prefix); |
| } |
| } |
| } |