From a633224ad9541eb8a3cd4d1cb9cb723983a70807 Mon Sep 17 00:00:00 2001 From: Santiago Seifert Date: Wed, 10 May 2023 11:05:52 +0000 Subject: [PATCH 1/2] Allow apps with BLUETOOTH permission to control system routing They can already control system routing via BluetoothManager, but it allows them to do both Remote and System routing via MediaRouter2. Concretely, this change is needed for apps to access the id of system routes via MediaRouter2 in order to add info to the output switcher via RouteListingPreference. Bug: 274894524 Test: atest CtsMediaBetterTogetherTestCases Test: atest MediaRouter2HostSideTest Test: atest AudioPoliciesDeviceRouteControllerTest LegacyDeviceRouteControllerTest Change-Id: If9bbfc7574e637aa4c07701edf1f2b12e27e4f52 Merged-In: If9bbfc7574e637aa4c07701edf1f2b12e27e4f52 --- media/java/android/media/MediaRoute2Info.java | 24 +++++ .../AudioPoliciesDeviceRouteController.java | 13 ++- .../server/media/MediaRouter2ServiceImpl.java | 93 +++++++++++++++++-- .../server/media/MediaRouterService.java | 2 + .../media/SystemMediaRoute2Provider.java | 27 +++--- 5 files changed, 133 insertions(+), 26 deletions(-) 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; } } From 4ab85f3d6fced3b32d0a330e2fef7d2f96d37d79 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Fri, 12 May 2023 11:54:05 -0700 Subject: [PATCH 2/2] Fix misplaced surface from fold -> unfold while bubbles are expanded This also fixes the surface being misplaced when the theme changes. When setBounds is called in TaskViewTransitions we were skipping creating a new transition whenever there was anything pending. Instead, we should only skip if the pending in flight is for that task view AND an opening type transit, otherwise the bounds never get updated until the next open / if a bounds change is sent without any other pending. Test: manual - have an expanded bubble on folded device, unfold => ensure the expanded view is fully visible - have an expanded bubble & change the theme via QS => ensure the expanded view is properly placed - test various bubble animations: expand, collapse, switch, new bubble, dismissing while expanded, rotation and ensure they all look fine. Test: atest TaskViewTransitionTest Bug: 281855397 Bug: 280440749 Change-Id: I43cbb90bdfbe90bf068c5de21d060193aa38c96f --- .../shell/taskview/TaskViewTransitions.java | 35 +++++- .../taskview/TaskViewTransitionsTest.java | 101 +++++++++++++++++- 2 files changed, 131 insertions(+), 5 deletions(-) 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);