diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index fe2faaf79a1a2..2e7fca3f2b469 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -134,6 +134,21 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { return null; } + /** + * Looks through the pending transitions for a opening transaction that matches the provided + * `taskView`. + * @param taskView the pending transition should be for this. + */ + private PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) { + for (int i = mPending.size() - 1; i >= 0; --i) { + if (mPending.get(i).mTaskView != taskView) continue; + if (TransitionUtil.isOpeningType(mPending.get(i).mType)) { + return mPending.get(i); + } + } + return null; + } + /** * Looks through the pending transitions for one matching `taskView`. * @param taskView the pending transition should be for this. @@ -149,6 +164,19 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { return null; } + /** + * Returns all the pending transitions for a given `taskView`. + * @param taskView the pending transition should be for this. + */ + ArrayList findAllPending(TaskViewTaskController taskView) { + ArrayList list = new ArrayList<>(); + for (int i = mPending.size() - 1; i >= 0; --i) { + if (mPending.get(i).mTaskView != taskView) continue; + list.add(mPending.get(i)); + } + return list; + } + private PendingTransition findPending(IBinder claimed) { for (int i = 0; i < mPending.size(); ++i) { if (mPending.get(i).mClaimed != claimed) continue; @@ -249,9 +277,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // Task view isn't visible, the bounds will next visibility update. return; } - if (hasPending()) { - // There is already a transition in-flight, the window bounds will be set in - // prepareOpenAnimation. + PendingTransition pendingOpen = findPendingOpeningTransition(taskView); + if (pendingOpen != null) { + // There is already an opening transition in-flight, the window bounds will be + // set in prepareOpenAnimation (via the window crop) if needed. return; } WindowContainerTransaction wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index 9d566860c1cdc..71ad0d79eaca3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -45,6 +45,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -58,6 +60,12 @@ public class TaskViewTransitionsTest extends ShellTestCase { ActivityManager.RunningTaskInfo mTaskInfo; @Mock WindowContainerToken mToken; + @Mock + TaskViewTaskController mTaskViewTaskController2; + @Mock + ActivityManager.RunningTaskInfo mTaskInfo2; + @Mock + WindowContainerToken mToken2; TaskViewTransitions mTaskViewTransitions; @@ -73,10 +81,16 @@ public class TaskViewTransitionsTest extends ShellTestCase { mTaskInfo.token = mToken; mTaskInfo.taskId = 314; mTaskInfo.taskDescription = mock(ActivityManager.TaskDescription.class); + when(mTaskViewTaskController.getTaskInfo()).thenReturn(mTaskInfo); + + mTaskInfo2 = new ActivityManager.RunningTaskInfo(); + mTaskInfo2.token = mToken2; + mTaskInfo2.taskId = 315; + mTaskInfo2.taskDescription = mock(ActivityManager.TaskDescription.class); + when(mTaskViewTaskController2.getTaskInfo()).thenReturn(mTaskInfo2); mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions)); mTaskViewTransitions.addTaskView(mTaskViewTaskController); - when(mTaskViewTaskController.getTaskInfo()).thenReturn(mTaskInfo); } @Test @@ -119,7 +133,7 @@ public class TaskViewTransitionsTest extends ShellTestCase { } @Test - public void testSetTaskBounds_taskVisibleWithPending_noTransaction() { + public void testSetTaskBounds_taskVisibleWithPendingOpen_noTransaction() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true); @@ -134,6 +148,43 @@ public class TaskViewTransitionsTest extends ShellTestCase { .isNull(); } + @Test + public void testSetTaskBounds_taskVisibleWithPendingChange_transition() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true); + + // Consume the pending transition from visibility change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + // Verify it was consumed + TaskViewTransitions.PendingTransition checkPending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(checkPending).isNull(); + + // Test that set bounds creates a new transition + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 100, 100)); + assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE)) + .isNotNull(); + + // Test that set bounds again (with different bounds) creates another transition + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 300, 200)); + List pendingList = + mTaskViewTransitions.findAllPending(mTaskViewTaskController) + .stream() + .filter(pendingTransition -> pendingTransition.mType == TRANSIT_CHANGE) + .toList(); + assertThat(pendingList.size()).isEqualTo(2); + } + @Test public void testSetTaskBounds_sameBounds_noTransaction() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); @@ -161,6 +212,16 @@ public class TaskViewTransitionsTest extends ShellTestCase { mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE); assertThat(pendingBounds).isNotNull(); + // Test that setting same bounds with in-flight transition doesn't cause another one + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 100, 100)); + List pendingList = + mTaskViewTransitions.findAllPending(mTaskViewTaskController) + .stream() + .filter(pendingTransition -> pendingTransition.mType == TRANSIT_CHANGE) + .toList(); + assertThat(pendingList.size()).isEqualTo(1); + // Consume the pending bounds transaction mTaskViewTransitions.startAnimation(pendingBounds.mClaimed, mock(TransitionInfo.class), @@ -180,6 +241,42 @@ public class TaskViewTransitionsTest extends ShellTestCase { assertThat(pendingBounds2).isNull(); } + + @Test + public void testSetTaskBounds_taskVisibleWithDifferentTaskViewPendingChange_transition() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.addTaskView(mTaskViewTaskController2); + + mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true); + + // Consume the pending transition from visibility change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + // Verify it was consumed + TaskViewTransitions.PendingTransition checkPending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(checkPending).isNull(); + + // Set the second taskview as visible & check that it has a pending transition + mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController2, true); + TaskViewTransitions.PendingTransition pending2 = + mTaskViewTransitions.findPending(mTaskViewTaskController2, TRANSIT_TO_FRONT); + assertThat(pending2).isNotNull(); + + // Test that set bounds on the first taskview will create a new transition + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 100, 100)); + assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE)) + .isNotNull(); + } + @Test public void testSetTaskVisibility_taskRemoved_noNPE() { mTaskViewTransitions.removeTaskView(mTaskViewTaskController); diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 7e238e44043bb..0e9c162e4929f 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -57,6 +57,30 @@ public final class MediaRoute2Info implements Parcelable { } }; + /** + * The {@link #getOriginalId() original id} of the route that represents the built-in media + * route. + * + *

A route with this id will only be visible to apps with permission to do system routing, + * which means having {@link android.Manifest.permission#BLUETOOTH_CONNECT} and {@link + * android.Manifest.permission#BLUETOOTH_SCAN}, or {@link + * android.Manifest.permission#MODIFY_AUDIO_ROUTING}. + * + * @hide + */ + public static final String ROUTE_ID_DEVICE = "DEVICE_ROUTE"; + + /** + * The {@link #getOriginalId() original id} of the route that represents the default system + * media route. + * + *

A route with this id will be visible to apps with no permission over system routing. See + * {@link #ROUTE_ID_DEVICE} for details. + * + * @hide + */ + public static final String ROUTE_ID_DEFAULT = "DEFAULT_ROUTE"; + /** @hide */ @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING, CONNECTION_STATE_CONNECTED}) diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java index 182aa6fcef028..93f6ff3de3c28 100644 --- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java +++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java @@ -42,13 +42,10 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; - /* package */ final class AudioPoliciesDeviceRouteController implements DeviceRouteController { private static final String TAG = "APDeviceRoutesController"; - private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; - @NonNull private final Context mContext; @NonNull @@ -182,10 +179,12 @@ import java.util.Objects; synchronized (this) { return new MediaRoute2Info.Builder( - DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) - .setVolumeHandling(mAudioManager.isVolumeFixed() - ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED - : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) + MediaRoute2Info.ROUTE_ID_DEVICE, + mContext.getResources().getText(name).toString()) + .setVolumeHandling( + mAudioManager.isVolumeFixed() + ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED + : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .setVolume(mDeviceVolume) .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) .setType(type) diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index cac22a6b8b208..b79991e5d0132 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -28,6 +28,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa 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; @@ -75,8 +76,10 @@ 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; @@ -97,6 +100,17 @@ class MediaRouter2ServiceImpl { 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. + * + *

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, @@ -142,6 +156,7 @@ class MediaRouter2ServiceImpl { } }; + @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) /* package */ MediaRouter2ServiceImpl(Context context) { mContext = context; mActivityManager = mContext.getSystemService(ActivityManager.class); @@ -155,12 +170,28 @@ class MediaRouter2ServiceImpl { 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 affectedRouter = + mAllRouterRecords.values().stream().filter(it -> it.mUid == uid).findFirst(); + if (affectedRouter.isPresent()) { + affectedRouter.get().maybeUpdateSystemRoutingPermissionLocked(); + } + } + } + // Start of methods that implement MediaRouter2 operations. @NonNull @@ -1511,6 +1542,7 @@ class MediaRouter2ServiceImpl { public final int mPid; public final boolean mHasConfigureWifiDisplayPermission; public final boolean mHasModifyAudioRoutingPermission; + public final AtomicBoolean mHasBluetoothRoutingPermission; public final int mRouterId; public RouteDiscoveryPreference mDiscoveryPreference; @@ -1528,15 +1560,47 @@ class MediaRouter2ServiceImpl { 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; + return mHasModifyAudioRoutingPermission || mHasBluetoothRoutingPermission.get(); + } + + public void maybeUpdateSystemRoutingPermissionLocked() { + boolean oldSystemRoutingPermissionValue = hasSystemRoutingPermission(); + mHasBluetoothRoutingPermission.set(fetchBluetoothPermission()); + boolean newSystemRoutingPermissionValue = hasSystemRoutingPermission(); + if (oldSystemRoutingPermissionValue != newSystemRoutingPermissionValue) { + Map routesToReport = + newSystemRoutingPermissionValue + ? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters + : mUserRecord.mHandler.mLastNotifiedRoutesToNonPrivilegedRouters; + notifyRoutesUpdated(routesToReport.values().stream().toList()); + + List sessionInfos = + mUserRecord.mHandler.mSystemProvider.getSessionInfos(); + RoutingSessionInfo systemSessionToReport = + newSystemRoutingPermissionValue && !sessionInfos.isEmpty() + ? sessionInfos.get(0) + : mUserRecord.mHandler.mSystemProvider.getDefaultSessionInfo(); + notifySessionInfoChanged(systemSessionToReport); + } } public void dispose() { @@ -1559,6 +1623,14 @@ class MediaRouter2ServiceImpl { 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); @@ -1580,6 +1652,19 @@ class MediaRouter2ServiceImpl { } } + /** + * Sends the corresponding router an update for the given session. + * + *

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. @@ -2471,11 +2556,7 @@ class MediaRouter2ServiceImpl { @NonNull List routerRecords, @NonNull RoutingSessionInfo sessionInfo) { for (RouterRecord routerRecord : routerRecords) { - try { - routerRecord.mRouter.notifySessionInfoChanged(sessionInfo); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); - } + routerRecord.notifySessionInfoChanged(sessionInfo); } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 4d134b69811db..b440e8815c167 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -18,6 +18,7 @@ package com.android.server.media; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -137,6 +138,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub private final String mDefaultAudioRouteId; private final String mBluetoothA2dpRouteId; + @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) public MediaRouterService(Context context) { mService2 = new MediaRouter2ServiceImpl(context); mContext = context; diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 6d2d2e405ab90..426bc5eed0515 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -55,7 +55,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { SystemMediaRoute2Provider.class.getPackage().getName(), SystemMediaRoute2Provider.class.getName()); - static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE"; static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION"; private final AudioManager mAudioManager; @@ -170,7 +169,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Bundle sessionHints) { // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with // a route ID different from the default route ID. The service should've filtered. - if (TextUtils.equals(routeId, DEFAULT_ROUTE_ID)) { + if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo); return; } @@ -213,7 +212,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { @Override public void transferToRoute(long requestId, String sessionId, String routeId) { - if (TextUtils.equals(routeId, DEFAULT_ROUTE_ID)) { + if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { // The currently selected route is the default route. return; } @@ -326,10 +325,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { builder.addTransferableRoute(deviceRoute.getId()); } mSelectedRouteId = selectedRoute.getId(); - mDefaultRoute = new MediaRoute2Info.Builder(DEFAULT_ROUTE_ID, selectedRoute) - .setSystemRoute(true) - .setProviderId(mUniqueId) - .build(); + mDefaultRoute = + new MediaRoute2Info.Builder(MediaRoute2Info.ROUTE_ID_DEFAULT, selectedRoute) + .setSystemRoute(true) + .setProviderId(mUniqueId) + .build(); builder.addSelectedRoute(mSelectedRouteId); for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) { @@ -363,12 +363,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } mSessionInfos.clear(); mSessionInfos.add(newSessionInfo); - mDefaultSessionInfo = new RoutingSessionInfo.Builder( - SYSTEM_SESSION_ID, "" /* clientPackageName */) - .setProviderId(mUniqueId) - .setSystemSession(true) - .addSelectedRoute(DEFAULT_ROUTE_ID) - .build(); + mDefaultSessionInfo = + new RoutingSessionInfo.Builder( + SYSTEM_SESSION_ID, "" /* clientPackageName */) + .setProviderId(mUniqueId) + .setSystemSession(true) + .addSelectedRoute(MediaRoute2Info.ROUTE_ID_DEFAULT) + .build(); return true; } }