diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java index 68558fa993..0fb9718124 100644 --- a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java +++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java @@ -31,6 +31,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.drawable.ColorDrawable; @@ -47,6 +48,7 @@ import android.widget.Button; import android.widget.TextView; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.BaseActivity; @@ -58,7 +60,6 @@ import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.views.GoOverviewActionsView; import com.android.quickstep.views.TaskContainer; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.recents.model.ThumbnailData; import java.lang.annotation.Retention; @@ -131,7 +132,7 @@ public final class TaskOverlayFactoryGo extends TaskOverlayFactory { * Called when the current task is interactive for the user */ @Override - public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix, + public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix, boolean rotated) { if (mDialog != null && mDialog.isShowing()) { // Redraw the dialog in case the layout changed diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java index 358d703b75..46501c4fc8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -245,11 +245,20 @@ public final class KeyboardQuickSwitchController implements } void updateThumbnailInBackground(Task task, Consumer callback) { - mModel.getThumbnailCache().updateThumbnailInBackground(task, callback); + mModel.getThumbnailCache().getThumbnailInBackground(task, + thumbnailData -> { + task.thumbnail = thumbnailData; + callback.accept(thumbnailData); + }); } void updateIconInBackground(Task task, Consumer callback) { - mModel.getIconCache().updateIconInBackground(task, callback); + mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> { + task.icon = icon; + task.titleDescription = contentDescription; + task.title = title; + callback.accept(task); + }); } void onCloseComplete() { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 21a826870e..6c3b4adc09 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -43,6 +43,8 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_N import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; import static com.android.wm.shell.Flags.enableTinyTaskbar; +import static java.lang.invoke.MethodHandles.Lookup.PROTECTED; + import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.app.ActivityOptions; @@ -1515,7 +1517,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mIsNavBarKidsMode && isThreeButtonNav(); } - protected boolean isNavBarForceVisible() { + @VisibleForTesting(otherwise = PROTECTED) + public boolean isNavBarForceVisible() { return mIsNavBarForceVisible; } @@ -1649,6 +1652,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mControllers.uiController.canToggleHomeAllApps(); } + boolean isIconAlignedWithHotseat() { + return mControllers.uiController.isIconAlignedWithHotseat(); + } + @VisibleForTesting public TaskbarControllers getControllers() { return mControllers; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java index abd5fae1f3..dd141098c6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java @@ -159,6 +159,10 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { if (mHoverView == null || mToolTipText == null) { return; } + // Do not show tooltip if taskbar icons are transitioning to hotseat. + if (mActivity.isIconAlignedWithHotseat()) { + return; + } if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) { return; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index ee79fbf201..b90e5fd33f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -115,6 +115,7 @@ public class TaskbarManager { private WindowManager mWindowManager; private FrameLayout mTaskbarRootLayout; private boolean mAddedWindow; + private boolean mIsSuspended; private final TaskbarNavButtonController mNavButtonController; private final ComponentCallbacks mComponentCallbacks; @@ -443,6 +444,8 @@ public class TaskbarManager { */ @VisibleForTesting public synchronized void recreateTaskbar() { + if (mIsSuspended) return; + Trace.beginSection("recreateTaskbar"); try { DeviceProfile dp = mUserUnlocked ? @@ -648,8 +651,22 @@ public class TaskbarManager { } } + /** + * Removes Taskbar from the window manager and prevents recreation if {@code true}. + *

+ * Suspending is for testing purposes only; avoid calling this method in production. + */ @VisibleForTesting - public void addTaskbarRootViewToWindow() { + public void setSuspended(boolean isSuspended) { + mIsSuspended = isSuspended; + if (mIsSuspended) { + removeTaskbarRootViewFromWindow(); + } else { + addTaskbarRootViewToWindow(); + } + } + + private void addTaskbarRootViewToWindow() { if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) { mWindowManager.addView(mTaskbarRootLayout, mTaskbarActivityContext.getWindowLayoutParams()); @@ -657,8 +674,7 @@ public class TaskbarManager { } } - @VisibleForTesting - public void removeTaskbarRootViewFromWindow() { + private void removeTaskbarRootViewFromWindow() { if (enableTaskbarNoRecreate() && mAddedWindow) { mWindowManager.removeViewImmediate(mTaskbarRootLayout); mAddedWindow = false; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt index 0a81f78c98..36828a8fe9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt @@ -24,11 +24,9 @@ import com.android.launcher3.util.CancellableTask import com.android.quickstep.RecentsModel import com.android.quickstep.util.DesktopTask import com.android.quickstep.util.GroupTask -import com.android.systemui.shared.recents.model.Task import com.android.window.flags.Flags.enableDesktopWindowingMode import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps import java.io.PrintWriter -import java.util.function.Consumer /** * Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior: @@ -185,9 +183,16 @@ class TaskbarRecentAppsController( for (groupTask in shownTasks) { for (task in groupTask.tasks) { - val callback = - Consumer { controllers.taskbarViewController.onTaskUpdated(it) } - val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback) + val cancellableTask = + recentsModel.iconCache.getIconInBackground(task) { + icon, + contentDescription, + title -> + task.icon = icon + task.titleDescription = contentDescription + task.title = title + controllers.taskbarViewController.onTaskUpdated(task) + } if (cancellableTask != null) { iconLoadRequests.add(cancellableTask) } diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java index 13e98444f4..18461a6440 100644 --- a/quickstep/src/com/android/quickstep/RecentsActivity.java +++ b/quickstep/src/com/android/quickstep/RecentsActivity.java @@ -132,6 +132,13 @@ public final class RecentsActivity extends StatefulActivity implem * Init drag layer and overview panel views. */ protected void setupViews() { + SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this); + // SplitSelectStateController needs to be created before setContentView() + mSplitSelectStateController = + new SplitSelectStateController(this, mHandler, getStateManager(), + null /* depthController */, getStatsLogManager(), + systemUiProxy, RecentsModel.INSTANCE.get(this), + null /*activityBackCallback*/); inflateRootView(R.layout.fallback_recents_activity); setContentView(getRootView()); mDragLayer = findViewById(R.id.drag_layer); @@ -139,12 +146,6 @@ public final class RecentsActivity extends StatefulActivity implem mFallbackRecentsView = findViewById(R.id.overview_panel); mActionsView = findViewById(R.id.overview_actions_view); getRootView().getSysUiScrim().getSysUIProgress().updateValue(0); - SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this); - mSplitSelectStateController = - new SplitSelectStateController(this, mHandler, getStateManager(), - null /* depthController */, getStatsLogManager(), - systemUiProxy, RecentsModel.INSTANCE.get(this), - null /*activityBackCallback*/); mDragLayer.recreateControllers(); if (enableDesktopWindowingMode()) { mDesktopRecentsTransitionController = new DesktopRecentsTransitionController( diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java index e6febff5f7..b3a9199bf9 100644 --- a/quickstep/src/com/android/quickstep/TaskIconCache.java +++ b/quickstep/src/com/android/quickstep/TaskIconCache.java @@ -55,7 +55,6 @@ import com.android.systemui.shared.recents.model.Task.TaskKey; import com.android.systemui.shared.system.PackageManagerWrapper; import java.util.concurrent.Executor; -import java.util.function.Consumer; /** * Manages the caching of task icons and related data. @@ -103,21 +102,21 @@ public class TaskIconCache implements DisplayInfoChangeListener { * @param callback The callback to receive the task after its data has been populated. * @return A cancelable handle to the request */ - public CancellableTask updateIconInBackground(Task task, Consumer callback) { + public CancellableTask getIconInBackground(Task task, GetTaskIconCallback callback) { Preconditions.assertUIThread(); if (task.icon != null) { // Nothing to load, the icon is already loaded - callback.accept(task); + callback.onTaskIconReceived(task.icon, task.titleDescription, task.title); return null; } CancellableTask request = new CancellableTask<>( () -> getCacheEntry(task), MAIN_EXECUTOR, result -> { - task.icon = result.icon; - task.titleDescription = result.contentDescription; - task.title = result.title; - callback.accept(task); + callback.onTaskIconReceived( + result.icon, + result.contentDescription, + result.title); dispatchIconUpdate(task.key.id); } ); @@ -280,6 +279,12 @@ public class TaskIconCache implements DisplayInfoChangeListener { public String title = ""; } + /** Callback used when retrieving app icons from cache. */ + public interface GetTaskIconCallback { + /** Called when task icon is retrieved. */ + void onTaskIconReceived(Drawable icon, String contentDescription, String title); + } + void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) { mTaskVisualsChangeListener = newListener; } diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index c243a24b96..80902e32c8 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -52,7 +52,6 @@ import com.android.quickstep.views.RecentsViewContainer; import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.recents.model.ThumbnailData; import java.util.ArrayList; import java.util.List; @@ -185,17 +184,6 @@ public class TaskOverlayFactory implements ResourceBasedOverride { return mTaskContainer.getTaskView(); } - /** - * Called when the current task is interactive for the user - * - * @deprecated TODO(b/350931107): Remove this interface once TaskOverlayFactoryGo is updated - */ - @Deprecated - public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix, - boolean rotated) { - initOverlay(task, thumbnail.getThumbnail(), matrix, rotated); - } - /** * Called when the current task is interactive for the user */ diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java index 38e927f95f..3c6c3e4a94 100644 --- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java +++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java @@ -131,8 +131,7 @@ public class TaskThumbnailCache implements TaskThumbnailDataSource { Preconditions.assertUIThread(); // Fetch the thumbnail for this task and put it in the cache if (task.thumbnail == null) { - updateThumbnailInBackground(task.key, lowResolution, - t -> task.thumbnail = t); + getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t); } } @@ -145,13 +144,13 @@ public class TaskThumbnailCache implements TaskThumbnailDataSource { } /** - * Asynchronously fetches the icon and other task data for the given {@param task}. + * Asynchronously fetches the thumbnail for the given {@code task}. * * @param callback The callback to receive the task after its data has been populated. * @return A cancelable handle to the request */ @Override - public CancellableTask updateThumbnailInBackground( + public CancellableTask getThumbnailInBackground( Task task, @NonNull Consumer callback) { Preconditions.assertUIThread(); @@ -164,10 +163,7 @@ public class TaskThumbnailCache implements TaskThumbnailDataSource { return null; } - return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> { - task.thumbnail = t; - callback.accept(t); - }); + return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback); } /** @@ -187,7 +183,7 @@ public class TaskThumbnailCache implements TaskThumbnailDataSource { return newSize > oldSize; } - private CancellableTask updateThumbnailInBackground(TaskKey key, + private CancellableTask getThumbnailInBackground(TaskKey key, boolean lowResolution, Consumer callback) { Preconditions.assertUIThread(); diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt index b21a1b4add..9f3ef4adb2 100644 --- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt @@ -67,14 +67,15 @@ class TasksRepository( this.visibleTaskIds.value = visibleTaskIdList.toSet() } - /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */ + /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */ private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = flow { emit(task.key.id to task.thumbnail) val thumbnailDataResult: ThumbnailData? = suspendCancellableCoroutine { continuation -> val cancellableTask = - taskThumbnailDataSource.updateThumbnailInBackground(task) { + taskThumbnailDataSource.getThumbnailInBackground(task) { + task.thumbnail = it continuation.resume(it) } continuation.invokeOnCancellation { cancellableTask?.cancel() } @@ -94,12 +95,7 @@ class TasksRepository( tasks.filter { it.key.id in visibleIds } } val visibleThumbnailDataRequests: Flow> = - visibleTasks.map { - it.map { visibleTask -> - val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail } - getThumbnailDataRequest(taskCopy) - } - } + visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) } return visibleThumbnailDataRequests.flatMapLatest { thumbnailRequestFlows: List -> if (thumbnailRequestFlows.isEmpty()) { diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt index 55598f0a2d..986acbeb31 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt @@ -22,7 +22,7 @@ import com.android.systemui.shared.recents.model.ThumbnailData import java.util.function.Consumer interface TaskThumbnailDataSource { - fun updateThumbnailInBackground( + fun getThumbnailInBackground( task: Task, callback: Consumer ): CancellableTask? diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java index 5e42b9001b..27fb31de73 100644 --- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java +++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java @@ -138,12 +138,13 @@ public class SplitWithKeyboardShortcutController { mLauncher, mLauncher.getDragLayer(), controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(), null /* icon */, startingTaskRect); + Task task = Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo, + false /* isLocked */); RecentsModel.INSTANCE.get(mLauncher.getApplicationContext()) .getIconCache() - .updateIconInBackground( - Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo, - false /* isLocked */), - (task) -> floatingTaskView.setIcon(task.icon)); + .getIconInBackground( + task, + (icon, contentDescription, title) -> floatingTaskView.setIcon(icon)); floatingTaskView.setAlpha(1); floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index 888b24af4f..9977d30fe9 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -846,7 +846,8 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.thumbnailCache - .updateThumbnailInBackground(it.task) { thumbnailData -> + .getThumbnailInBackground(it.task) { thumbnailData -> + it.task.thumbnail = thumbnailData it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData) } ?.also { request -> pendingThumbnailLoadRequests.add(request) } @@ -862,12 +863,15 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.iconCache - .updateIconInBackground(it.task) { task -> - setIcon(it.iconView, task.icon) + .getIconInBackground(it.task) { icon, contentDescription, title -> + it.task.icon = icon + it.task.titleDescription = contentDescription + it.task.title = title + setIcon(it.iconView, icon) if (enableOverviewIconMenu()) { - setText(it.iconView, task.title) + setText(it.iconView, title) } - it.digitalWellBeingToast?.initialize(task) + it.digitalWellBeingToast?.initialize(it.task) } ?.also { request -> pendingIconLoadRequests.add(request) } } else { diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt index 9ecd9353bb..2f0b44604e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt @@ -20,7 +20,6 @@ import android.animation.AnimatorTestRule import android.content.ComponentName import android.content.Intent import android.os.Process -import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.BubbleTextView import com.android.launcher3.appprediction.PredictionRowView @@ -34,6 +33,7 @@ import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.util.TestUtil import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -55,17 +55,17 @@ class TaskbarAllAppsControllerTest { @InjectController lateinit var overlayController: TaskbarOverlayController @Test - @UiThreadTest fun testToggle_once_showsAllApps() { - allAppsController.toggle() + getInstrumentation().runOnMainSync { allAppsController.toggle() } assertThat(allAppsController.isOpen).isTrue() } @Test - @UiThreadTest fun testToggle_twice_closesAllApps() { - allAppsController.toggle() - allAppsController.toggle() + getInstrumentation().runOnMainSync { + allAppsController.toggle() + allAppsController.toggle() + } assertThat(allAppsController.isOpen).isFalse() } @@ -77,54 +77,62 @@ class TaskbarAllAppsControllerTest { } @Test - @UiThreadTest fun testSetApps_beforeOpened_cachesInfo() { - allAppsController.setApps(TEST_APPS, 0, emptyMap()) - allAppsController.toggle() + val overlayContext = + TestUtil.getOnUiThread { + allAppsController.setApps(TEST_APPS, 0, emptyMap()) + allAppsController.toggle() + overlayController.requestWindow() + } - val overlayContext = overlayController.requestWindow() assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS) } @Test - @UiThreadTest fun testSetApps_afterOpened_updatesStore() { - allAppsController.toggle() - allAppsController.setApps(TEST_APPS, 0, emptyMap()) + val overlayContext = + TestUtil.getOnUiThread { + allAppsController.toggle() + allAppsController.setApps(TEST_APPS, 0, emptyMap()) + overlayController.requestWindow() + } - val overlayContext = overlayController.requestWindow() assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS) } @Test - @UiThreadTest fun testSetPredictedApps_beforeOpened_cachesInfo() { - allAppsController.setPredictedApps(TEST_PREDICTED_APPS) - allAppsController.toggle() - val predictedApps = - overlayController - .requestWindow() - .appsView - .floatingHeaderView - .findFixedRowByType(PredictionRowView::class.java) - .predictedApps + TestUtil.getOnUiThread { + allAppsController.setPredictedApps(TEST_PREDICTED_APPS) + allAppsController.toggle() + + overlayController + .requestWindow() + .appsView + .floatingHeaderView + .findFixedRowByType(PredictionRowView::class.java) + .predictedApps + } + assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS) } @Test - @UiThreadTest fun testSetPredictedApps_afterOpened_cachesInfo() { - allAppsController.toggle() - allAppsController.setPredictedApps(TEST_PREDICTED_APPS) - val predictedApps = - overlayController - .requestWindow() - .appsView - .floatingHeaderView - .findFixedRowByType(PredictionRowView::class.java) - .predictedApps + TestUtil.getOnUiThread { + allAppsController.toggle() + allAppsController.setPredictedApps(TEST_PREDICTED_APPS) + + overlayController + .requestWindow() + .appsView + .floatingHeaderView + .findFixedRowByType(PredictionRowView::class.java) + .predictedApps + } + assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS) } @@ -140,36 +148,38 @@ class TaskbarAllAppsControllerTest { } // Ensure the recycler view fully inflates before trying to grab an icon. - getInstrumentation().runOnMainSync { - val btv = + val btv = + TestUtil.getOnUiThread { overlayController .requestWindow() .appsView .activeRecyclerView .findViewHolderForAdapterPosition(0) ?.itemView as? BubbleTextView - assertThat(btv?.hasDot()).isTrue() - } + } + assertThat(btv?.hasDot()).isTrue() } @Test - @UiThreadTest fun testUpdateNotificationDots_predictedApp_hasDot() { - allAppsController.setPredictedApps(TEST_PREDICTED_APPS) - allAppsController.toggle() + getInstrumentation().runOnMainSync { + allAppsController.setPredictedApps(TEST_PREDICTED_APPS) + allAppsController.toggle() + taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted( + PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]), + NotificationKeyData("key"), + ) + } - taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted( - PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]), - NotificationKeyData("key"), - ) - - val predictionRowView = - overlayController - .requestWindow() - .appsView - .floatingHeaderView - .findFixedRowByType(PredictionRowView::class.java) - val btv = predictionRowView.getChildAt(0) as BubbleTextView + val btv = + TestUtil.getOnUiThread { + overlayController + .requestWindow() + .appsView + .floatingHeaderView + .findFixedRowByType(PredictionRowView::class.java) + .getChildAt(0) as BubbleTextView + } assertThat(btv.hasDot()).isTrue() } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt index fae5562d55..f946d4d482 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt @@ -18,7 +18,6 @@ package com.android.launcher3.taskbar.overlay import android.app.ActivityManager.RunningTaskInfo import android.view.MotionEvent -import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP @@ -31,7 +30,7 @@ import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.android.launcher3.views.BaseDragLayer +import com.android.launcher3.util.TestUtil.getOnUiThread import com.android.systemui.shared.system.TaskStackChangeListeners import com.google.common.truth.Truth.assertThat import org.junit.Rule @@ -54,74 +53,69 @@ class TaskbarOverlayControllerTest { get() = taskbarUnitTestRule.activityContext @Test - @UiThreadTest fun testRequestWindow_twice_reusesWindow() { - val context1 = overlayController.requestWindow() - val context2 = overlayController.requestWindow() + val (context1, context2) = + getOnUiThread { + Pair(overlayController.requestWindow(), overlayController.requestWindow()) + } assertThat(context1).isSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() { - val context1 = overlayController.requestWindow() - overlayController.hideWindow() + val context1 = getOnUiThread { overlayController.requestWindow() } + getInstrumentation().runOnMainSync { overlayController.hideWindow() } - val context2 = overlayController.requestWindow() + val context2 = getOnUiThread { overlayController.requestWindow() } assertThat(context1).isNotSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_afterHidingOverlay_createsNewWindow() { - val context1 = overlayController.requestWindow() - TestOverlayView.show(context1) - overlayController.hideWindow() + val context1 = getOnUiThread { overlayController.requestWindow() } + getInstrumentation().runOnMainSync { + TestOverlayView.show(context1) + overlayController.hideWindow() + } - val context2 = overlayController.requestWindow() + val context2 = getOnUiThread { overlayController.requestWindow() } assertThat(context1).isNotSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_addsProxyView() { - TestOverlayView.show(overlayController.requestWindow()) + getInstrumentation().runOnMainSync { + TestOverlayView.show(overlayController.requestWindow()) + } assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() } @Test - @UiThreadTest fun testRequestWindow_closeProxyView_closesOverlay() { - val overlay = TestOverlayView.show(overlayController.requestWindow()) - AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + getInstrumentation().runOnMainSync { + AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + } assertThat(overlay.isOpen).isFalse() } @Test fun testRequestWindow_attachesDragLayer() { - lateinit var dragLayer: BaseDragLayer<*> - getInstrumentation().runOnMainSync { - dragLayer = overlayController.requestWindow().dragLayer - } - + val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer } // Allow drag layer to attach before checking. getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() } } @Test - @UiThreadTest fun testHideWindow_closesOverlay() { - val overlay = TestOverlayView.show(overlayController.requestWindow()) - overlayController.hideWindow() + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + getInstrumentation().runOnMainSync { overlayController.hideWindow() } assertThat(overlay.isOpen).isFalse() } @Test fun testHideWindow_detachesDragLayer() { - lateinit var dragLayer: BaseDragLayer<*> - getInstrumentation().runOnMainSync { - dragLayer = overlayController.requestWindow().dragLayer - } + val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer } // Wait for drag layer to be attached to window before hiding. getInstrumentation().runOnMainSync { @@ -131,26 +125,30 @@ class TaskbarOverlayControllerTest { } @Test - @UiThreadTest fun testTwoOverlays_closeOne_windowStaysOpen() { - val context = overlayController.requestWindow() - val overlay1 = TestOverlayView.show(context) - val overlay2 = TestOverlayView.show(context) + val (overlay1, overlay2) = + getOnUiThread { + val context = overlayController.requestWindow() + Pair(TestOverlayView.show(context), TestOverlayView.show(context)) + } - overlay1.close(false) + getInstrumentation().runOnMainSync { overlay1.close(false) } assertThat(overlay2.isOpen).isTrue() assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() } @Test - @UiThreadTest fun testTwoOverlays_closeAll_closesWindow() { - val context = overlayController.requestWindow() - val overlay1 = TestOverlayView.show(context) - val overlay2 = TestOverlayView.show(context) + val (overlay1, overlay2) = + getOnUiThread { + val context = overlayController.requestWindow() + Pair(TestOverlayView.show(context), TestOverlayView.show(context)) + } - overlay1.close(false) - overlay2.close(false) + getInstrumentation().runOnMainSync { + overlay1.close(false) + overlay2.close(false) + } assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() } @@ -165,11 +163,7 @@ class TaskbarOverlayControllerTest { @Test fun testTaskMovedToFront_closesOverlay() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - } - + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo()) // Make sure TaskStackChangeListeners' Handler posts the callback before checking state. getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } @@ -177,9 +171,8 @@ class TaskbarOverlayControllerTest { @Test fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() { - lateinit var overlay: TestOverlayView + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) taskbarContext.controllers.sharedState?.allAppsVisible = false } @@ -189,9 +182,8 @@ class TaskbarOverlayControllerTest { @Test fun testTaskStackChanged_allAppsOpen_closesOverlay() { - lateinit var overlay: TestOverlayView + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) taskbarContext.controllers.sharedState?.allAppsVisible = true } @@ -200,33 +192,39 @@ class TaskbarOverlayControllerTest { } @Test - @UiThreadTest fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() { - val overlayContext = overlayController.requestWindow() - val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP } + val context = getOnUiThread { overlayController.requestWindow() } + val overlay = getOnUiThread { + TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP } + } - overlayController.updateLauncherDeviceProfile( - overlayController.launcherDeviceProfile - .toBuilder(overlayContext) - .setGestureMode(false) - .build() - ) + getInstrumentation().runOnMainSync { + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(context) + .setGestureMode(false) + .build() + ) + } assertThat(overlay.isOpen).isFalse() } @Test - @UiThreadTest fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() { - val overlayContext = overlayController.requestWindow() - val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS } + val context = getOnUiThread { overlayController.requestWindow() } + val overlay = getOnUiThread { + TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS } + } - overlayController.updateLauncherDeviceProfile( - overlayController.launcherDeviceProfile - .toBuilder(overlayContext) - .setGestureMode(false) - .build() - ) + getInstrumentation().runOnMainSync { + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(context) + .setGestureMode(false) + .build() + ) + } assertThat(overlay.isOpen).isTrue() } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt index 6638736ff6..c48947e078 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt @@ -16,6 +16,7 @@ package com.android.launcher3.taskbar.rules +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode import com.android.launcher3.util.DisplayController @@ -59,23 +60,25 @@ class TaskbarModeRule(private val context: TaskbarWindowSandboxContext) : TestRu override fun evaluate() { val mode = taskbarMode.mode - context.applicationContext.putObject( - DisplayController.INSTANCE, - object : DisplayController(context) { - override fun getInfo(): Info { - return spy(super.getInfo()) { - on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT) - on { isPinnedTaskbar } doReturn (mode == Mode.PINNED) - on { navigationMode } doReturn - when (mode) { - Mode.TRANSIENT, - Mode.PINNED -> NavigationMode.NO_BUTTON - Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS - } + getInstrumentation().runOnMainSync { + context.applicationContext.putObject( + DisplayController.INSTANCE, + object : DisplayController(context) { + override fun getInfo(): Info { + return spy(super.getInfo()) { + on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT) + on { isPinnedTaskbar } doReturn (mode == Mode.PINNED) + on { navigationMode } doReturn + when (mode) { + Mode.TRANSIENT, + Mode.PINNED -> NavigationMode.NO_BUTTON + Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS + } + } } - } - }, - ) + }, + ) + } base.evaluate() } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt index 12f946e730..8a64949f7d 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt @@ -20,18 +20,25 @@ import android.app.Instrumentation import android.app.PendingIntent import android.content.IIntentSender import android.content.Intent +import android.provider.Settings +import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ServiceTestRule import com.android.launcher3.LauncherAppState import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.taskbar.TaskbarManager import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric +import com.android.launcher3.util.ModelTestExtensions.loadModelSync +import com.android.launcher3.util.TestUtil import com.android.quickstep.AllAppsActionManager import com.android.quickstep.TouchInteractionService import com.android.quickstep.TouchInteractionService.TISBinder import org.junit.Assume.assumeTrue +import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -48,12 +55,11 @@ import org.junit.runners.model.Statement * that code that is executed on the main thread in production should also happen on that thread * when tested. * - * `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test - * executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will - * prevent those messages from being processed until after the test body finishes. + * `@UiThreadTest` is incompatible with this rule. The annotation causes this rule to run on the + * main thread, but it needs to be run on the test thread for it to work properly. Instead, only run + * code that requires the main thread using something like [Instrumentation.runOnMainSync] or + * [TestUtil.getOnUiThread]. * - * To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform - * only sections of the test body on the main thread synchronously: * ``` * @Test * fun example() { @@ -71,6 +77,10 @@ class TaskbarUnitTestRule( private val instrumentation = InstrumentationRegistry.getInstrumentation() private val serviceTestRule = ServiceTestRule() + private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE) + private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE) + private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule) + private lateinit var taskbarManager: TaskbarManager val activityContext: TaskbarActivityContext @@ -80,15 +90,34 @@ class TaskbarUnitTestRule( } override fun apply(base: Statement, description: Description): Statement { + return settingRules.apply(createStatement(base, description), description) + } + + private fun createStatement(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { + // Only run test when Taskbar is enabled. instrumentation.runOnMainSync { assumeTrue( LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent ) } + // Process secure setting annotations. + instrumentation.runOnMainSync { + userSetupCompleteRule.putInt( + if (description.getAnnotation(UserSetupMode::class.java) != null) { + 0 + } else { + 1 + } + ) + kidsModeRule.putInt( + if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0 + ) + } + // Check for existing Taskbar instance from Launcher process. val launcherTaskbarManager: TaskbarManager? = if (!isRunningInRobolectric) { @@ -105,8 +134,8 @@ class TaskbarUnitTestRule( null } - instrumentation.runOnMainSync { - taskbarManager = + taskbarManager = + TestUtil.getOnUiThread { TaskbarManager( context, AllAppsActionManager(context, UI_HELPER_EXECUTOR) { @@ -114,12 +143,14 @@ class TaskbarUnitTestRule( }, object : TaskbarNavButtonCallbacks {}, ) - } + } try { + LauncherAppState.getInstance(context).model.loadModelSync() + // Replace Launcher Taskbar window with test instance. instrumentation.runOnMainSync { - launcherTaskbarManager?.removeTaskbarRootViewFromWindow() + launcherTaskbarManager?.setSuspended(true) taskbarManager.onUserUnlocked() // Required to complete initialization. } @@ -129,7 +160,7 @@ class TaskbarUnitTestRule( // Revert Taskbar window. instrumentation.runOnMainSync { taskbarManager.destroy() - launcherTaskbarManager?.addTaskbarRootViewToWindow() + launcherTaskbarManager?.setSuspended(false) } } } @@ -167,4 +198,35 @@ class TaskbarUnitTestRule( @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FIELD) annotation class InjectController + + /** Overrides [USER_SETUP_COMPLETE] to be `false` for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class UserSetupMode + + /** Overrides [NAV_BAR_KIDS_MODE] to be `true` for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class NavBarKidsMode + + /** Rule for Taskbar integer-based secure settings. */ + private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule { + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + val originalValue = + Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0) + try { + base.evaluate() + } finally { + instrumentation.runOnMainSync { putInt(originalValue) } + } + } + } + } + + /** Puts [value] into secure settings under [settingName]. */ + fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value) + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt index 8262e0f23b..234e4991c6 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt @@ -22,6 +22,8 @@ import com.android.launcher3.taskbar.TaskbarKeyguardController import com.android.launcher3.taskbar.TaskbarManager import com.android.launcher3.taskbar.TaskbarStashController import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices import com.google.common.truth.Truth.assertThat @@ -125,9 +127,40 @@ class TaskbarUnitTestRuleTest { } } - /** Executes [runTest] after the [testRule] setup phase completes. */ + @Test + fun testUserSetupMode_default_isComplete() { + onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() } + } + + @Test + fun testUserSetupMode_withAnnotation_isIncomplete() { + @UserSetupMode class Mode + onSetup(description = Description.createSuiteDescription(Mode::class.java)) { + assertThat(activityContext.isUserSetupComplete).isFalse() + } + } + + @Test + fun testNavBarKidsMode_default_navBarNotForcedVisible() { + onSetup { assertThat(activityContext.isNavBarForceVisible).isFalse() } + } + + @Test + fun testNavBarKidsMode_withAnnotation_navBarForcedVisible() { + @NavBarKidsMode class Mode + onSetup(description = Description.createSuiteDescription(Mode::class.java)) { + assertThat(activityContext.isNavBarForceVisible).isTrue() + } + } + + /** + * Executes [runTest] after the [testRule] setup phase completes. + * + * A [description] can also be provided to mimic annotating a test or test class. + */ private fun onSetup( testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context), + description: Description = DESCRIPTION, runTest: TaskbarUnitTestRule.() -> Unit, ) { testRule @@ -135,7 +168,7 @@ class TaskbarUnitTestRuleTest { object : Statement() { override fun evaluate() = runTest(testRule) }, - DESCRIPTION, + description, ) .evaluate() } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt index b66b7351bf..30fc4916a8 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt @@ -32,7 +32,7 @@ class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { var shouldLoadSynchronously: Boolean = true /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */ - override fun updateThumbnailInBackground( + override fun getThumbnailInBackground( task: Task, callback: Consumer ): CancellableTask? { diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java index 3232bdb4e0..ef3a833856 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java @@ -87,6 +87,7 @@ public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer); when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper()); when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay); + when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false); when(mTaskbarDragLayer.getChildCount()).thenReturn(1); mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null)); @@ -243,6 +244,21 @@ public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { false); } + @Test + public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() { + when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true); + + boolean hoverHandled = + mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent); + waitForIdleSync(); + + assertThat(hoverHandled).isTrue(); + verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, + true); + } + private void waitForIdleSync() { mTestableLooper.processAllMessages(); } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index d90580158b..5c052b2f70 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -771,6 +771,7 @@ public class Launcher extends StatefulActivity // initialized properly. onSaveInstanceState(new Bundle()); mModel.rebindCallbacks(); + updateDisallowBack(); } finally { Trace.endSection(); } diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 19a3002665..a296f46ef4 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -380,6 +380,28 @@ public final class Utilities { outPivot.y = dst.top + dst.height() * pivotYPct; } + /** + * Scales a {@code RectF} in place about a specified pivot point. + * + *

This method modifies the given {@code RectF} directly to scale it proportionally + * by the given {@code scale}, while preserving its center at the specified + * {@code (pivotX, pivotY)} coordinates. + * + * @param rectF the {@code RectF} to scale, modified directly. + * @param pivotX the x-coordinate of the pivot point about which to scale. + * @param pivotY the y-coordinate of the pivot point about which to scale. + * @param scale the factor by which to scale the rectangle. Values less than 1 will + * shrink the rectangle, while values greater than 1 will enlarge it. + */ + public static void scaleRectFAboutPivot(RectF rectF, float pivotX, float pivotY, float scale) { + rectF.offset(-pivotX, -pivotY); + rectF.left *= scale; + rectF.top *= scale; + rectF.right *= scale; + rectF.bottom *= scale; + rectF.offset(pivotX, pivotY); + } + /** * Maps t from one range to another range. * @param t The value to map. diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java index 32445ecddb..870c8769ca 100644 --- a/src/com/android/launcher3/apppairs/AppPairIcon.java +++ b/src/com/android/launcher3/apppairs/AppPairIcon.java @@ -18,10 +18,12 @@ package com.android.launcher3.apppairs; import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.FloatProperty; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -54,6 +56,26 @@ import java.util.function.Predicate; public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable { private static final String TAG = "AppPairIcon"; + // The duration of the scaling animation on hover enter/exit. + private static final int HOVER_SCALE_DURATION = 150; + // The default scale of the icon when not hovered. + private static final Float HOVER_SCALE_DEFAULT = 1f; + // The max scale of the icon when hovered. + private static final Float HOVER_SCALE_MAX = 1.1f; + // Animates the scale of the icon background on hover. + private static final FloatProperty HOVER_SCALE_PROPERTY = + new FloatProperty<>("hoverScale") { + @Override + public void setValue(AppPairIcon view, float scale) { + view.mIconGraphic.setHoverScale(scale); + } + + @Override + public Float get(AppPairIcon view) { + return view.mIconGraphic.getHoverScale(); + } + }; + // A view that holds the app pair icon graphic. private AppPairIconGraphic mIconGraphic; // A view that holds the app pair's title. @@ -250,4 +272,14 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + + @Override + public void onHoverChanged(boolean hovered) { + super.onHoverChanged(hovered); + ObjectAnimator + .ofFloat(this, HOVER_SCALE_PROPERTY, + hovered ? HOVER_SCALE_MAX : HOVER_SCALE_DEFAULT) + .setDuration(HOVER_SCALE_DURATION) + .start(); + } } diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java index db83d91a4b..114ed2edd5 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java @@ -26,6 +26,7 @@ import android.os.Build; import androidx.annotation.NonNull; +import com.android.launcher3.Utilities; import com.android.launcher3.icons.FastBitmapDrawable; /** @@ -128,6 +129,18 @@ public class AppPairIconDrawable extends Drawable { height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); + // Scale each background from its center edge closest to the center channel. + Utilities.scaleRectFAboutPivot( + leftSide, + leftSide.left + leftSide.width(), + leftSide.top + leftSide.centerY(), + mP.getHoverScale()); + Utilities.scaleRectFAboutPivot( + rightSide, + rightSide.left, + rightSide.top + rightSide.centerY(), + mP.getHoverScale()); + drawCustomRoundedRect(canvas, leftSide, new float[]{ mP.getBigRadius(), mP.getBigRadius(), mP.getSmallRadius(), mP.getSmallRadius(), @@ -163,6 +176,18 @@ public class AppPairIconDrawable extends Drawable { height - (mP.getStandardIconPadding() + mP.getOuterPadding()) ); + // Scale each background from its center edge closest to the center channel. + Utilities.scaleRectFAboutPivot( + topSide, + topSide.left + topSide.centerX(), + topSide.top + topSide.height(), + mP.getHoverScale()); + Utilities.scaleRectFAboutPivot( + bottomSide, + bottomSide.left + bottomSide.centerX(), + bottomSide.top, + mP.getHoverScale()); + drawCustomRoundedRect(canvas, topSide, new float[]{ mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), mP.getBigRadius(), diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt index 45dc013348..5b546d69de 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt @@ -64,6 +64,8 @@ class AppPairIconDrawingParams(val context: Context, container: Int) { var isLeftRightSplit: Boolean = true // The background paint color (based on container). var bgColor: Int = 0 + // The scale of the icon background while hovered. + var hoverScale: Float = 1f init { val activity: ActivityContext = ActivityContext.lookupContext(context) diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt index dce97eb594..034b686828 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt @@ -139,4 +139,19 @@ constructor(context: Context, attrs: AttributeSet? = null) : super.dispatchDraw(canvas) drawable.draw(canvas) } + + /** + * Sets the scale of the icon background while hovered. + */ + fun setHoverScale(scale: Float) { + drawParams.hoverScale = scale + redraw() + } + + /** + * Gets the scale of the icon background while hovered. + */ + fun getHoverScale(): Float { + return drawParams.hoverScale + } } diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java index d6533171d3..5dee3221c9 100644 --- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java +++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java @@ -16,6 +16,8 @@ package com.android.launcher3.ui.workspace; import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME; +import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; +import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -38,6 +40,7 @@ import com.android.launcher3.tapl.HomeAppIcon; import com.android.launcher3.tapl.HomeAppIconMenuItem; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.rule.TestStabilityRule; import org.junit.Test; @@ -111,6 +114,7 @@ public class TaplThemeIconsTest extends AbstractLauncherUiTest { } @Test + @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998 public void testShortcutIconWithTheme() throws Exception { setThemeEnabled(true); initialize(this);