Merge "Allow trackpad to tune RAPID_DECELERATION_FACTOR for gesture nav" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 21b9863..a779641 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -316,3 +316,16 @@
description: "Archived apps will use new icon in app title"
bug: "350758155"
}
+
+flag {
+ name: "enable_multi_instance_menu_taskbar"
+ namespace: "launcher"
+ description: "Menu in Taskbar with options to launch and manage multiple instances of the same app"
+ bug: "355237285"
+}
+flag {
+ name: "navigate_to_child_preference"
+ namespace: "launcher"
+ description: "Settings screen supports navigating to child preference if the key is not on the screen"
+ bug: "293390881"
+}
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index b1fe89e..784a094 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -39,4 +39,16 @@
android:background="@color/overview_foreground_scrim_color"
android:alpha="0" />
+ <FrameLayout
+ android:id="@+id/splash_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/splash_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:importantForAccessibility="no" />
+ </FrameLayout>
</com.android.quickstep.task.thumbnail.TaskThumbnailView>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 63412e9..8bcbb33 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -299,10 +299,16 @@
<string name="taskbar_button_quick_settings">Quick Settings</string>
<!-- Accessibility title for the Taskbar window. [CHAR_LIMIT=NONE] -->
<string name="taskbar_a11y_title">Taskbar</string>
- <!-- Accessibility title for the Taskbar window appeared. [CHAR_LIMIT=NONE] -->
+ <!-- Accessibility title for the Taskbar window appeared. [CHAR_LIMIT=30] -->
<string name="taskbar_a11y_shown_title">Taskbar shown</string>
- <!-- Accessibility title for the Taskbar window being close. [CHAR_LIMIT=NONE] -->
+ <!-- Accessibility title for the Taskbar window appearing together with bubble bar on left. [CHAR_LIMIT=30] -->
+ <string name="taskbar_a11y_shown_with_bubbles_left_title">Taskbar & bubbles left shown</string>
+ <!-- Accessibility title for the Taskbar window appearing together with bubble bar on right. [CHAR_LIMIT=30] -->
+ <string name="taskbar_a11y_shown_with_bubbles_right_title">Taskbar & bubbles right shown</string>
+ <!-- Accessibility title for the Taskbar window being closed. [CHAR_LIMIT=30] -->
<string name="taskbar_a11y_hidden_title">Taskbar hidden</string>
+ <!-- Accessibility title for the Taskbar window being closed together with bubble bar. [CHAR_LIMIT=30] -->
+ <string name="taskbar_a11y_hidden_with_bubbles_title">Taskbar & bubbles hidden</string>
<!-- Accessibility title for the Taskbar window on phones. [CHAR_LIMIT=NONE] -->
<string name="taskbar_phone_a11y_title">Navigation bar</string>
<!-- Text in popup dialog for user to switch between always showing Taskbar or not. [CHAR LIMIT=30] -->
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 44601b7..fe1b403 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1252,15 +1252,25 @@
TransitionFilter homeCheck = new TransitionFilter();
// No need to handle the transition that also dismisses keyguard.
homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
homeCheck.mRequirements =
new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
+ new TransitionFilter.Requirement(),
new TransitionFilter.Requirement()};
+
homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName();
homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
+
homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+
+ homeCheck.mRequirements[2].mNot = true;
+ homeCheck.mRequirements[2].mCustomAnimation = true;
+ homeCheck.mRequirements[2].mMustBeTask = true;
+ homeCheck.mRequirements[2].mMustBeIndependent = true;
+
SystemUiProxy.INSTANCE.get(mLauncher)
.registerRemoteTransition(mLauncherOpenTransition, homeCheck);
if (mBackAnimationController != null) {
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index a16031d..92d9516 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -290,6 +290,9 @@
writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled);
writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused);
writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow);
- writer.println(prefix + "\tmPredictedApps: " + mPredictedApps);
+ writer.println(prefix + "\tmPredictedApps: " + mPredictedApps.size());
+ for (WorkspaceItemInfo info : mPredictedApps) {
+ writer.println(prefix + "\t\t" + info);
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index de974ec..c50e82d 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -536,6 +536,9 @@
writer.println(prefix + "HotseatPredictionController");
writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags));
writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount);
- writer.println(prefix + "\tmPredictedItems: " + mPredictedItems);
+ writer.println(prefix + "\tmPredictedItems: " + mPredictedItems.size());
+ for (ItemInfo info : mPredictedItems) {
+ writer.println(prefix + "\t\t" + info);
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 93a023d..252ebf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -272,6 +272,10 @@
* app launch animation.
*/
public void setIgnoreInAppFlagForSync(boolean enabled) {
+ if (mControllers == null) {
+ // This method can be called before init() is called.
+ return;
+ }
mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c42d6c6..e58069a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -72,6 +72,7 @@
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import java.util.List;
import java.util.function.Predicate;
@@ -246,12 +247,34 @@
@Override
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
- announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
+ announceTaskbarShown();
} else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
- announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
+ announceTaskbarHidden();
}
return super.performAccessibilityActionInternal(action, arguments);
+ }
+ private void announceTaskbarShown() {
+ BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
+ if (bubbleBarLocation == null) {
+ announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title));
+ } else if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ announceForAccessibility(
+ mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_left_title));
+ } else {
+ announceForAccessibility(
+ mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_right_title));
+ }
+ }
+
+ private void announceTaskbarHidden() {
+ BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
+ if (bubbleBarLocation == null) {
+ announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
+ } else {
+ announceForAccessibility(
+ mContext.getString(R.string.taskbar_a11y_hidden_with_bubbles_title));
+ }
}
protected void announceAccessibilityChanges() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 3c646cb..e6cac2f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -23,8 +23,12 @@
import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.internal.jank.Cuj;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Callbacks for {@link TaskbarView} to interact with its controller.
@@ -104,4 +108,18 @@
mControllers.taskbarScrimViewController.onTaskbarVisibilityChanged(
mTaskbarView.getVisibility());
}
+
+ /**
+ * Get current location of bubble bar, if it is visible.
+ * Returns {@code null} if bubble bar is not shown.
+ */
+ @Nullable
+ public BubbleBarLocation getBubbleBarLocationIfVisible() {
+ BubbleBarViewController bubbleBarViewController =
+ mControllers.bubbleControllers.map(c -> c.bubbleBarViewController).orElse(null);
+ if (bubbleBarViewController != null && bubbleBarViewController.isBubbleBarVisible()) {
+ return bubbleBarViewController.getBubbleBarLocation();
+ }
+ return null;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 5d550ae..af371f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -307,6 +307,17 @@
}
}
+ @Override
+ public void setAlpha(float alpha) {
+ super.setAlpha(alpha);
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View childView = getChildAt(i);
+ if (!(childView instanceof BubbleView)) continue;
+ ((BubbleView) childView).setProvideShadowOutline(alpha == 1f);
+ }
+ }
+
/**
* Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders.
*
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 3bcaa16..6db42a4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -70,6 +70,8 @@
// The current scale value of the dot
private float mDotScale;
+ private boolean mProvideShadowOutline = true;
+
// TODO: (b/273310265) handle RTL
// Whether the bubbles are positioned on the left or right side of the screen
private boolean mOnLeft = false;
@@ -113,17 +115,28 @@
});
}
+ //TODO(b/345490679) remove once proper shadow is applied
+ /** Set whether provide an outline. */
+ public void setProvideShadowOutline(boolean provideOutline) {
+ if (mProvideShadowOutline == provideOutline) return;
+ mProvideShadowOutline = provideOutline;
+ invalidateOutline();
+ }
+
private void getOutline(Outline outline) {
updateBubbleSizeAndDotRender();
final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
final int inset = (mBubbleSize - normalizedSize) / 2;
- outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+ if (mProvideShadowOutline) {
+ outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+ }
}
private void updateBubbleSizeAndDotRender() {
int updatedBubbleSize = Math.min(getWidth(), getHeight());
if (updatedBubbleSize == mBubbleSize) return;
mBubbleSize = updatedBubbleSize;
+ invalidateOutline();
if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
new file mode 100644
index 0000000..feed2fd
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+/**
+ * Container to hold [com.android.launcher3.DeviceProfile] related to Recents.
+ *
+ * @property isLargeScreen whether the current device posture has a large screen
+ */
+data class RecentsDeviceProfile(
+ val isLargeScreen: Boolean,
+ val widthPx: Int,
+ val heightPx: Int,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
index adf904c..13cf56d 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt
@@ -16,21 +16,6 @@
package com.android.quickstep.recents.data
-import com.android.quickstep.views.RecentsViewContainer
-
-/**
- * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains
- * data related to Recents.
- */
-class RecentsDeviceProfileRepository(private val container: RecentsViewContainer) {
-
- fun getRecentsDeviceProfile() =
- with(container.deviceProfile) { RecentsDeviceProfile(isLargeScreen = isTablet) }
-
- /**
- * Container to hold [com.android.launcher3.DeviceProfile] related to Recents.
- *
- * @property isLargeScreen whether the current device posture has a large screen
- */
- data class RecentsDeviceProfile(val isLargeScreen: Boolean)
+interface RecentsDeviceProfileRepository {
+ fun getRecentsDeviceProfile(): RecentsDeviceProfile
}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
new file mode 100644
index 0000000..ce39ff1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.views.RecentsViewContainer
+
+/**
+ * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains
+ * data related to Recents.
+ */
+class RecentsDeviceProfileRepositoryImpl(private val container: RecentsViewContainer) :
+ RecentsDeviceProfileRepository {
+
+ override fun getRecentsDeviceProfile() =
+ with(container.deviceProfile) {
+ RecentsDeviceProfile(isLargeScreen = isTablet, widthPx = widthPx, heightPx = heightPx)
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt
new file mode 100644
index 0000000..2c2a744
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.view.Surface
+
+/**
+ * Container to hold orientation/rotation related information related to Recents.
+ *
+ * @property activityRotation rotation of the activity hosting RecentsView
+ */
+data class RecentsRotationState(
+ @Surface.Rotation val activityRotation: Int,
+ @Surface.Rotation val orientationHandlerRotation: Int,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
index 6ead704..ed074d2 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt
@@ -16,20 +16,6 @@
package com.android.quickstep.recents.data
-import com.android.quickstep.util.RecentsOrientedState
-
-/**
- * Repository for [RecentsRotationState] which holds orientation/rotation related information
- * related to Recents
- */
-class RecentsRotationStateRepository(private val state: RecentsOrientedState) {
- fun getRecentsRotationState() =
- with(state) { RecentsRotationState(activityRotation = recentsActivityRotation) }
-
- /**
- * Container to hold orientation/rotation related information related to Recents.
- *
- * @property activityRotation rotation of the activity hosting RecentsView
- */
- data class RecentsRotationState(val activityRotation: Int)
+interface RecentsRotationStateRepository {
+ fun getRecentsRotationState(): RecentsRotationState
}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt
new file mode 100644
index 0000000..8417b06
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * Repository for [RecentsRotationState] which holds orientation/rotation related information
+ * related to Recents
+ */
+class RecentsRotationStateRepositoryImpl(private val state: RecentsOrientedState) :
+ RecentsRotationStateRepository {
+ override fun getRecentsRotationState() =
+ with(state) {
+ RecentsRotationState(
+ activityRotation = recentsActivityRotation,
+ orientationHandlerRotation = orientationHandler.rotation
+ )
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index f73db5a..71be75b 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -130,9 +130,15 @@
icon,
contentDescription,
title ->
- continuation.resume(
- TaskIconQueryResponse(icon, contentDescription, title)
- )
+ icon.constantState?.let {
+ continuation.resume(
+ TaskIconQueryResponse(
+ it.newDrawable().mutate(),
+ contentDescription,
+ title
+ )
+ )
+ }
}
continuation.invokeOnCancellation { cancellableTask?.cancel() }
}
@@ -157,7 +163,7 @@
}
}
-private data class TaskIconQueryResponse(
+data class TaskIconQueryResponse(
val icon: Drawable,
val contentDescription: String,
val title: String
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 3a6d5c0..eba7688 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -26,6 +26,9 @@
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.GetSplashSizeUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
@@ -33,7 +36,6 @@
import com.android.quickstep.task.viewmodel.TaskViewModel
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
-import java.util.logging.Level
internal typealias RecentsScopeId = String
@@ -145,13 +147,16 @@
TaskViewData(taskViewType)
}
TaskContainerData::class.java -> TaskContainerData()
+ TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
TaskThumbnailViewModel::class.java ->
TaskThumbnailViewModel(
recentsViewData = inject(),
taskViewData = inject(scopeId, extras),
- taskContainerData = inject(),
+ taskContainerData = inject(scopeId),
getThumbnailPositionUseCase = inject(),
- tasksRepository = inject()
+ tasksRepository = inject(),
+ splashAlphaUseCase = inject(scopeId),
+ getSplashSizeUseCase = inject(scopeId),
)
TaskOverlayViewModel::class.java -> {
val task = extras["Task"] as Task
@@ -171,6 +176,20 @@
rotationStateRepository = inject(),
tasksRepository = inject()
)
+ SplashAlphaUseCase::class.java ->
+ SplashAlphaUseCase(
+ recentsViewData = inject(),
+ taskContainerData = inject(scopeId),
+ taskThumbnailViewData = inject(scopeId),
+ tasksRepository = inject(),
+ rotationStateRepository = inject(),
+ )
+ GetSplashSizeUseCase::class.java ->
+ GetSplashSizeUseCase(
+ taskThumbnailViewData = inject(scopeId),
+ taskViewData = inject(scopeId, extras),
+ deviceProfileRepository = inject(),
+ )
else -> {
log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
error("Factory for ${modelClass.simpleName} not defined!")
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index fdb62df..f5e0243 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -31,4 +31,9 @@
// The settled set of visible taskIds that is updated after RecentsView scroll settles.
val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
+
+ // Color tint on foreground scrim
+ val tintAmount = MutableStateFlow(0f)
+
+ val thumbnailSplashProgress = MutableStateFlow(0f)
}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 8b03a84..6148d4b 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -45,4 +45,12 @@
fun setOverlayEnabled(isOverlayEnabled: Boolean) {
recentsViewData.overlayEnabled.value = isOverlayEnabled
}
+
+ fun setTintAmount(tintAmount: Float) {
+ recentsViewData.tintAmount.value = tintAmount
+ }
+
+ fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) {
+ recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
+ }
}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
index 8b8bc3e..168c1e0 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
@@ -19,13 +19,20 @@
import android.graphics.Bitmap
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
class TaskContainerViewModel(
private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase,
- private val getThumbnailUseCase: GetThumbnailUseCase
+ private val getThumbnailUseCase: GetThumbnailUseCase,
+ private val splashAlphaUseCase: SplashAlphaUseCase,
) {
fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId)
fun getSysUiStatusNavFlags(taskId: Int) =
sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId)
+
+ fun shouldShowThumbnailSplash(taskId: Int): Boolean =
+ (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt
new file mode 100644
index 0000000..145957a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCase.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.task.viewmodel.TaskViewData
+import kotlin.math.min
+
+class GetSplashSizeUseCase(
+ private val taskThumbnailViewData: TaskThumbnailViewData,
+ private val taskViewData: TaskViewData,
+ private val deviceProfileRepository: RecentsDeviceProfileRepository,
+) {
+ fun execute(splashImage: Drawable): Point {
+ val recentsDeviceProfile = deviceProfileRepository.getRecentsDeviceProfile()
+ val screenWidth = recentsDeviceProfile.widthPx
+ val screenHeight = recentsDeviceProfile.heightPx
+ val scaleAtFullscreen =
+ min(
+ screenWidth / taskThumbnailViewData.width.value,
+ screenHeight / taskThumbnailViewData.height.value,
+ )
+ val scaleFactor: Float = 1f / taskViewData.nonGridScale.value / scaleAtFullscreen
+ return Point(
+ (splashImage.intrinsicWidth * scaleFactor / taskThumbnailViewData.scaleX.value).toInt(),
+ (splashImage.intrinsicHeight * scaleFactor / taskThumbnailViewData.scaleY.value)
+ .toInt(),
+ )
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
new file mode 100644
index 0000000..e5618fc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Bitmap
+import android.view.Surface
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+class SplashAlphaUseCase(
+ private val recentsViewData: RecentsViewData,
+ private val taskContainerData: TaskContainerData,
+ private val taskThumbnailViewData: TaskThumbnailViewData,
+ private val tasksRepository: RecentTasksRepository,
+ private val rotationStateRepository: RecentsRotationStateRepository,
+) {
+ fun execute(taskId: Int): Flow<Float> =
+ combine(
+ taskThumbnailViewData.width,
+ taskThumbnailViewData.height,
+ tasksRepository.getThumbnailById(taskId),
+ taskContainerData.thumbnailSplashProgress,
+ recentsViewData.thumbnailSplashProgress
+ ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress ->
+ val thumbnail = thumbnailData?.thumbnail
+ when {
+ thumbnail == null -> 1f
+ taskSplashProgress > 0f -> taskSplashProgress
+ globalSplashProgress > 0f &&
+ isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) ->
+ globalSplashProgress
+ else -> 0f
+ }
+ }
+ .distinctUntilChanged()
+
+ private fun isInaccurateThumbnail(
+ thumbnail: Bitmap,
+ thumbnailRotation: Int,
+ width: Int,
+ height: Int
+ ): Boolean {
+ return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) ||
+ isThumbnailRotationDifferentFromTask(thumbnailRotation)
+ }
+
+ private fun isThumbnailAspectRatioDifferentFromThumbnailData(
+ thumbnail: Bitmap,
+ viewWidth: Int,
+ viewHeight: Int
+ ): Boolean {
+ val viewAspect: Float = viewWidth / viewHeight.toFloat()
+ val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat()
+ return Utilities.isRelativePercentDifferenceGreaterThan(
+ viewAspect,
+ thumbnailAspect,
+ PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT
+ )
+ }
+
+ private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
+ val rotationState = rotationStateRepository.getRecentsRotationState()
+ return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
+ (rotationState.activityRotation - thumbnailRotation) % 2 != 0
+ } else {
+ rotationState.orientationHandlerRotation != thumbnailRotation
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 3b3a811..aa7d26c 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -17,6 +17,9 @@
package com.android.quickstep.task.thumbnail
import android.graphics.Bitmap
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import android.view.Surface
import androidx.annotation.ColorInt
sealed class TaskThumbnailUiState {
@@ -26,8 +29,21 @@
data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
- data class Snapshot(val bitmap: Bitmap, @ColorInt val backgroundColor: Int) :
- TaskThumbnailUiState()
+ data class SnapshotSplash(
+ val snapshot: Snapshot,
+ val splash: Splash,
+ ) : TaskThumbnailUiState()
+
+ data class Snapshot(
+ val bitmap: Bitmap,
+ @Surface.Rotation val thumbnailRotation: Int,
+ @ColorInt val backgroundColor: Int
+ )
+
+ data class Splash(
+ val icon: Drawable?,
+ val size: Point,
+ )
}
data class TaskThumbnail(val taskId: Int, val isRunning: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index fcc2af3..41aee52 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -28,6 +28,7 @@
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.ViewPool
@@ -36,10 +37,12 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.util.TaskCornerRadius
import com.android.systemui.shared.system.QuickStepContract
+import kotlin.math.abs
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -50,6 +53,7 @@
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
+ private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this)
private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
private lateinit var viewAttachedScope: CoroutineScope
@@ -57,6 +61,8 @@
private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
private val thumbnailView: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+ private val splashContainer: FrameLayout by lazy { findViewById(R.id.splash_container) }
+ private val splashIcon: ImageView by lazy { findViewById(R.id.splash_icon) }
private var uiState: TaskThumbnailUiState = Uninitialized
private var inheritedScale: Float = 1f
@@ -92,16 +98,16 @@
when (viewModelUiState) {
is Uninitialized -> {}
is LiveTile -> drawLiveWindow()
- is Snapshot -> drawSnapshot(viewModelUiState)
+ is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
}
}
.launchIn(viewAttachedScope)
viewModel.dimProgress
- .onEach { dimProgress ->
- // TODO(b/348195366) Add fade in/out for scrim
- scrimView.alpha = dimProgress * MAX_SCRIM_ALPHA
- }
+ .onEach { dimProgress -> scrimView.alpha = dimProgress }
+ .launchIn(viewAttachedScope)
+ viewModel.splashAlpha
+ .onEach { splashAlpha -> splashContainer.alpha = splashAlpha }
.launchIn(viewAttachedScope)
viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
viewModel.inheritedScale
@@ -129,13 +135,35 @@
uiState = Uninitialized
}
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ if (changed) {
+ viewData.width.value = abs(right - left)
+ viewData.height.value = abs(bottom - top)
+ }
+ }
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
- if (uiState is Snapshot) {
+ if (uiState is SnapshotSplash) {
setImageMatrix()
}
}
+ override fun setScaleX(scaleX: Float) {
+ super.setScaleX(scaleX)
+ viewData.scaleX.value = scaleX
+ // Splash icon should ignore scale
+ splashIcon.scaleX = 1 / scaleX
+ }
+
+ override fun setScaleY(scaleY: Float) {
+ super.setScaleY(scaleY)
+ viewData.scaleY.value = scaleY
+ // Splash icon should ignore scale
+ splashIcon.scaleY = 1 / scaleY
+ }
+
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
@@ -147,6 +175,7 @@
private fun resetViews() {
liveTileView.isVisible = false
thumbnailView.isVisible = false
+ splashContainer.alpha = 0f
scrimView.alpha = 0f
setBackgroundColor(Color.BLACK)
}
@@ -159,6 +188,18 @@
liveTileView.isVisible = true
}
+ private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
+ drawSnapshot(snapshotSplash.snapshot)
+
+ splashContainer.isVisible = true
+ splashContainer.setBackgroundColor(snapshotSplash.snapshot.backgroundColor)
+ splashIcon.setImageDrawable(snapshotSplash.splash.icon)
+ splashIcon.updateLayoutParams<LayoutParams> {
+ width = snapshotSplash.splash.size.x
+ height = snapshotSplash.splash.size.y
+ }
+ }
+
private fun drawSnapshot(snapshot: Snapshot) {
drawBackground(snapshot.backgroundColor)
thumbnailView.setImageBitmap(snapshot.bitmap)
@@ -176,8 +217,4 @@
overviewCornerRadius,
fullscreenCornerRadius
) / inheritedScale
-
- private companion object {
- const val MAX_SCRIM_ALPHA = 0.4f
- }
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
new file mode 100644
index 0000000..1f8c0bc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class TaskThumbnailViewData {
+ val width = MutableStateFlow(0)
+ val height = MutableStateFlow(0)
+ val scaleX = MutableStateFlow(1f)
+ val scaleY = MutableStateFlow(1f)
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
index 769424c..5f2de94 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
@@ -20,4 +20,6 @@
class TaskContainerData {
val taskMenuOpenProgress = MutableStateFlow(0f)
+
+ val thumbnailSplashProgress = MutableStateFlow(0f)
}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index 6465645..de33919 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -18,18 +18,24 @@
import android.annotation.ColorInt
import android.graphics.Matrix
+import android.graphics.Point
import androidx.core.graphics.ColorUtils
import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.GetSplashSizeUseCase
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Splash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.systemui.shared.recents.model.Task
+import kotlin.math.max
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -47,9 +53,12 @@
taskViewData: TaskViewData,
taskContainerData: TaskContainerData,
private val tasksRepository: RecentTasksRepository,
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+ private val splashAlphaUseCase: SplashAlphaUseCase,
+ private val getSplashSizeUseCase: GetSplashSizeUseCase,
) {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
+ private val splashProgress = MutableStateFlow(flowOf(0f))
private lateinit var taskThumbnail: TaskThumbnail
/**
@@ -63,7 +72,13 @@
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
}
- val dimProgress: Flow<Float> = taskContainerData.taskMenuOpenProgress
+ val dimProgress: Flow<Float> =
+ combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
+ taskMenuOpenProgress,
+ tintAmount ->
+ max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
+ }
+ val splashAlpha = splashProgress.flatMapLatest { it }
val uiState: Flow<TaskThumbnailUiState> =
task
.flatMapLatest { taskFlow ->
@@ -73,10 +88,8 @@
taskThumbnail.isRunning -> LiveTile
isBackgroundOnly(taskVal) ->
BackgroundOnly(taskVal.colorBackground.removeAlpha())
- isSnapshotState(taskVal) -> {
- val bitmap = taskVal.thumbnail?.thumbnail!!
- Snapshot(bitmap, taskVal.colorBackground.removeAlpha())
- }
+ isSnapshotSplashState(taskVal) ->
+ SnapshotSplash(createSnapshotState(taskVal), createSplashState(taskVal))
else -> Uninitialized
}
}
@@ -86,6 +99,7 @@
fun bind(taskThumbnail: TaskThumbnail) {
this.taskThumbnail = taskThumbnail
task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
+ splashProgress.value = splashAlphaUseCase.execute(taskThumbnail.taskId)
}
fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
@@ -102,12 +116,28 @@
private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
- private fun isSnapshotState(task: Task): Boolean {
+ private fun isSnapshotSplashState(task: Task): Boolean {
val thumbnailPresent = task.thumbnail?.thumbnail != null
val taskLocked = task.isLocked
return thumbnailPresent && !taskLocked
}
+ private fun createSnapshotState(task: Task): Snapshot {
+ val thumbnailData = task.thumbnail
+ val bitmap = thumbnailData?.thumbnail!!
+ return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
+ }
+
+ private fun createSplashState(task: Task): Splash {
+ val taskIcon = task.icon
+ val size = if (taskIcon == null) Point() else getSplashSizeUseCase.execute(taskIcon)
+ return Splash(taskIcon, size)
+ }
+
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
+
+ private companion object {
+ const val MAX_SCRIM_ALPHA = 0.4f
+ }
}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
index 7a9ecf2..07dfc29 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
@@ -23,6 +23,8 @@
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
+ val nonGridScale = MutableStateFlow(1f)
+
// TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
/** Whether outline of TaskView is formed by outline thumbnail view(s). */
val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
index ec75d59..30ee360 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
@@ -22,4 +22,8 @@
fun updateScale(scale: Float) {
taskViewData.scale.value = scale
}
+
+ fun updateNonGridScale(nonGridScale: Float) {
+ taskViewData.nonGridScale.value = nonGridScale
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index e31a828..0335fa1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -187,7 +187,6 @@
) {
val snapshot = taskContainer.snapshotView
val iconView: View = taskContainer.iconView.asView()
- // TODO(334826842): Switch to splash state in TaskThumbnailView
if (!enableRefactorTaskThumbnail()) {
val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
builder.add(
@@ -198,6 +197,15 @@
)
)
thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
+ } else {
+ builder.add(
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ addUpdateListener {
+ taskContainer.taskContainerData.thumbnailSplashProgress.value =
+ it.animatedFraction
+ }
+ }
+ )
}
// With the new `IconAppChipView`, we always want to keep the chip pinned to the
// top left of the task / thumbnail.
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index d63ac56..8e232ee 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -193,7 +193,9 @@
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.recents.data.RecentTasksRepository;
import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl;
import com.android.quickstep.recents.data.RecentsRotationStateRepository;
+import com.android.quickstep.recents.data.RecentsRotationStateRepositoryImpl;
import com.android.quickstep.recents.di.RecentsDependencies;
import com.android.quickstep.recents.viewmodel.RecentsViewData;
import com.android.quickstep.recents.viewmodel.RecentsViewModel;
@@ -828,10 +830,10 @@
);
recentsDependencies.provide(RecentsRotationStateRepository.class,
- () -> new RecentsRotationStateRepository(mOrientationState));
+ () -> new RecentsRotationStateRepositoryImpl(mOrientationState));
recentsDependencies.provide(RecentsDeviceProfileRepository.class,
- () -> new RecentsDeviceProfileRepository(mContainer));
+ () -> new RecentsDeviceProfileRepositoryImpl(mContainer));
} else {
mRecentsViewModel = null;
}
@@ -3297,6 +3299,10 @@
}
private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
+ if (enableRefactorTaskThumbnail()) {
+ mRecentsViewModel.updateThumbnailSplashProgress(taskThumbnailSplashAlpha);
+ return;
+ }
int taskCount = getTaskViewCount();
if (taskCount == 0) {
return;
@@ -4894,7 +4900,6 @@
mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
primaryTaskSelected);
builder.addOnFrameCallback(() -> {
- // TODO(b/334826842): Handle splash icon for new TTV.
if (!enableRefactorTaskThumbnail()) {
taskContainer.getThumbnailViewDeprecated().refreshSplashView();
}
@@ -6116,7 +6121,6 @@
* tasks to be dimmed while other elements in the recents view are left alone.
*/
public void showForegroundScrim(boolean show) {
- // TODO(b/349601769) Add scrim response into new TTV - this is called from overlay
if (!show && mColorTint == 0) {
if (mTintingAnimator != null) {
mTintingAnimator.cancel();
@@ -6135,6 +6139,10 @@
private void setColorTint(float tintAmount) {
mColorTint = tintAmount;
+ if (enableRefactorTaskThumbnail()) {
+ mRecentsViewModel.setTintAmount(tintAmount);
+ }
+
for (int i = 0; i < getTaskViewCount(); i++) {
requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index e7a8720..b1a25b5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -69,7 +69,8 @@
private val taskContainerViewModel: TaskContainerViewModel by lazy {
TaskContainerViewModel(
sysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
- getThumbnailUseCase = RecentsDependencies.get()
+ getThumbnailUseCase = RecentsDependencies.get(),
+ splashAlphaUseCase = RecentsDependencies.get(),
)
}
@@ -81,7 +82,7 @@
val taskViewScope = RecentsDependencies.getScope(taskView)
linkTo(taskViewScope)
- val taskContainerScope = RecentsDependencies.getScope(this)
+ val taskContainerScope = RecentsDependencies.getScope(this@TaskContainer)
linkTo(taskContainerScope)
}
} else {
@@ -112,7 +113,8 @@
// TODO(b/334826842): Support shouldShowSplashView for new TTV.
val shouldShowSplashView: Boolean
get() =
- if (enableRefactorTaskThumbnail()) false
+ if (enableRefactorTaskThumbnail())
+ taskContainerViewModel.shouldShowThumbnailSplash(task.key.id)
else thumbnailViewDeprecated.shouldShowSplashView()
// TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index f2f036a..b2abe69 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -285,6 +285,9 @@
set(value) {
field = value
applyScale()
+ if (enableRefactorTaskThumbnail()) {
+ taskViewModel.updateNonGridScale(value)
+ }
}
private var dismissScale = 1f
@@ -1051,11 +1054,9 @@
if (isQuickSwitch) {
setFreezeRecentTasksReordering()
}
- // TODO(b/334826842) add splash functionality to new TTV
- if (!enableRefactorTaskThumbnail()) {
- disableStartingWindow =
- firstContainer.thumbnailViewDeprecated.shouldShowSplashView()
- }
+ // TODO(b/334826842) no work required - add splash functionality to new TTV -
+ // cold start e.g. restart device. Small splash moving to bigger splash
+ disableStartingWindow = firstContainer.shouldShowSplashView
}
Executors.UI_HELPER_EXECUTOR.execute {
if (
@@ -1396,7 +1397,7 @@
protected open fun refreshTaskThumbnailSplash() {
if (!enableRefactorTaskThumbnail()) {
- // TODO(b/334826842) add splash functionality to new TTV
+ // TODO(b/342560598) handle onTaskIconChanged
taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() }
}
}
@@ -1420,7 +1421,6 @@
protected open fun applyThumbnailSplashAlpha() {
if (!enableRefactorTaskThumbnail()) {
- // TODO(b/334826842) add splash functionality to new TTV
taskContainers.forEach {
it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java
new file mode 100644
index 0000000..0ff142a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class MultiStateCallbackTest {
+
+ private int mFlagCount = 0;
+ private int getNextStateFlag() {
+ int index = 1 << mFlagCount;
+ mFlagCount++;
+ return index;
+ }
+
+ private final MultiStateCallback mMultiStateCallback = new MultiStateCallback(new String[0]);
+ private final Runnable mCallback = spy(new Runnable() {
+ @Override
+ public void run() {}
+ });
+ private final Consumer<Boolean> mListener = spy(new Consumer<Boolean>() {
+ @Override
+ public void accept(Boolean isOn) {}
+ });
+
+ @Test
+ public void testSetState_trackedProperly() {
+ int watchedAnime = getNextStateFlag();
+
+ assertThat(mMultiStateCallback.getState()).isEqualTo(0);
+ assertThat(mMultiStateCallback.hasStates(watchedAnime)).isFalse();
+
+ mMultiStateCallback.setState(watchedAnime);
+
+ assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime);
+ assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue();
+ }
+
+ @Test
+ public void testSetState_withMultipleStates_trackedProperly() {
+ int watchedAnime = getNextStateFlag();
+ int sharedMemes = getNextStateFlag();
+
+ mMultiStateCallback.setState(watchedAnime);
+ mMultiStateCallback.setState(sharedMemes);
+
+ assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime | sharedMemes);
+ assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue();
+ assertThat(mMultiStateCallback.hasStates(sharedMemes)).isTrue();
+ assertThat(mMultiStateCallback.hasStates(watchedAnime | sharedMemes)).isTrue();
+ }
+
+ @Test
+ public void testClearState_trackedProperly() {
+ int lovedAnime = getNextStateFlag();
+
+ mMultiStateCallback.setState(lovedAnime);
+ mMultiStateCallback.clearState(lovedAnime);
+
+ assertThat(mMultiStateCallback.getState()).isEqualTo(0);
+ assertThat(mMultiStateCallback.hasStates(lovedAnime)).isFalse();
+ }
+
+ @Test
+ public void testClearState_withMultipleState_trackedProperly() {
+ int lovedAnime = getNextStateFlag();
+ int talkedAboutAnime = getNextStateFlag();
+
+ mMultiStateCallback.setState(lovedAnime);
+ mMultiStateCallback.setState(talkedAboutAnime);
+ mMultiStateCallback.clearState(talkedAboutAnime);
+
+ assertThat(mMultiStateCallback.getState()).isEqualTo(lovedAnime);
+ assertThat(mMultiStateCallback.hasStates(lovedAnime)).isTrue();
+ assertThat(mMultiStateCallback.hasStates(talkedAboutAnime)).isFalse();
+ assertThat(mMultiStateCallback.hasStates(lovedAnime | talkedAboutAnime)).isFalse();
+ }
+
+ @Test
+ public void testCallbackDoesNotRun_withoutState() {
+ int watchedOnePiece = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedOnePiece, mCallback);
+
+ verify(mCallback, never()).run();
+ }
+
+ @Test
+ public void testCallbackDoesNotRun_whenNotTracked() {
+ int watchedJujutsuKaisen = getNextStateFlag();
+
+ mMultiStateCallback.setState(watchedJujutsuKaisen);
+
+ verify(mCallback, never()).run();
+ }
+
+ @Test
+ public void testCallbackRuns_afterTrackedAndStateSet() {
+ int watchedHunterXHunter = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback);
+ mMultiStateCallback.setState(watchedHunterXHunter);
+
+ verify(mCallback, times(1)).run();
+ }
+
+ @Test
+ public void testCallbackRuns_onUiThread() {
+ int watchedHunterXHunter = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback);
+ mMultiStateCallback.setStateOnUiThread(watchedHunterXHunter);
+
+ runOnMainSync(() -> verify(mCallback, times(1)).run());
+ }
+
+ @Test
+ public void testCallbackRuns_agnosticallyToCallOrder() {
+ int watchedFullMetalAlchemist = getNextStateFlag();
+
+ mMultiStateCallback.setState(watchedFullMetalAlchemist);
+ mMultiStateCallback.runOnceAtState(watchedFullMetalAlchemist, mCallback);
+
+ verify(mCallback, times(1)).run();
+ }
+
+ @Test
+ public void testCallbackRuns_onlyOnceAfterStateSet() {
+ int watchedBleach = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedBleach, mCallback);
+ mMultiStateCallback.setState(watchedBleach);
+ mMultiStateCallback.setState(watchedBleach);
+
+ verify(mCallback, times(1)).run();
+ }
+
+ @Test
+ public void testCallbackRuns_onlyOnceAfterClearState() {
+ int rememberedGreatShow = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(rememberedGreatShow, mCallback);
+ mMultiStateCallback.setState(rememberedGreatShow);
+ mMultiStateCallback.clearState(rememberedGreatShow);
+ mMultiStateCallback.setState(rememberedGreatShow);
+
+ verify(mCallback, times(1)).run();
+ }
+
+ @Test
+ public void testCallbackDoesNotRun_withoutFullStateSet() {
+ int watchedMobPsycho = getNextStateFlag();
+ int watchedVinlandSaga = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedMobPsycho | watchedVinlandSaga, mCallback);
+ mMultiStateCallback.setState(watchedMobPsycho);
+
+ verify(mCallback, times(0)).run();
+ }
+
+ @Test
+ public void testCallbackRuns_withFullStateSet_agnosticallyToCallOrder() {
+ int watchedReZero = getNextStateFlag();
+ int watchedJojosBizareAdventure = getNextStateFlag();
+
+ mMultiStateCallback.setState(watchedJojosBizareAdventure);
+ mMultiStateCallback.runOnceAtState(watchedReZero | watchedJojosBizareAdventure, mCallback);
+ mMultiStateCallback.setState(watchedReZero);
+
+ verify(mCallback, times(1)).run();
+ }
+
+ @Test
+ public void testCallbackRuns_withFullStateSet_asIntegerMask() {
+ int watchedPokemon = getNextStateFlag();
+ int watchedDigimon = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedPokemon | watchedDigimon, mCallback);
+ mMultiStateCallback.setState(watchedPokemon | watchedDigimon);
+
+ verify(mCallback, times(1)).run();
+ }
+
+ @Test
+ public void testCallbackDoesNotRun_afterClearState() {
+ int watchedMonster = getNextStateFlag();
+ int watchedPingPong = getNextStateFlag();
+
+ mMultiStateCallback.runOnceAtState(watchedMonster | watchedPingPong, mCallback);
+ mMultiStateCallback.setState(watchedMonster);
+ mMultiStateCallback.clearState(watchedMonster);
+ mMultiStateCallback.setState(watchedPingPong);
+
+ verify(mCallback, times(0)).run();
+ }
+
+ @Test
+ public void testlistenerRuns_multipleTimes() {
+ int watchedSteinsGate = getNextStateFlag();
+
+ mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener);
+ mMultiStateCallback.setState(watchedSteinsGate);
+
+ // Called exactly one
+ verify(mListener, times(1)).accept(anyBoolean());
+ // Called exactly once with isOn = true
+ verify(mListener, times(1)).accept(eq(true));
+ // Never called with isOn = false
+ verify(mListener, times(0)).accept(eq(false));
+
+ mMultiStateCallback.clearState(watchedSteinsGate);
+
+ // Called exactly twice
+ verify(mListener, times(2)).accept(anyBoolean());
+ // Called exactly once with isOn = true
+ verify(mListener, times(1)).accept(eq(true));
+ // Called exactly once with isOn = false
+ verify(mListener, times(1)).accept(eq(false));
+ }
+
+ @Test
+ public void testlistenerDoesNotRun_forUnchangedState() {
+ int watchedSteinsGate = getNextStateFlag();
+
+ mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener);
+ mMultiStateCallback.setState(watchedSteinsGate);
+ mMultiStateCallback.setState(watchedSteinsGate);
+
+ // State remained unchanged
+ verify(mListener, times(1)).accept(anyBoolean());
+ // Called exactly once with isOn = true
+ verify(mListener, times(1)).accept(eq(true));
+ }
+
+ private static void runOnMainSync(Runnable runnable) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
new file mode 100644
index 0000000..cdfbd16
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository {
+ private var recentsDeviceProfile =
+ RecentsDeviceProfile(
+ isLargeScreen = false,
+ widthPx = 1080,
+ heightPx = 1920,
+ )
+
+ override fun getRecentsDeviceProfile() = recentsDeviceProfile
+
+ fun setRecentsDeviceProfile(newValue: RecentsDeviceProfile) {
+ recentsDeviceProfile = newValue
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt
new file mode 100644
index 0000000..c328672
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.view.Surface
+
+class FakeRecentsRotationStateRepository : RecentsRotationStateRepository {
+ private var recentsRotationState =
+ RecentsRotationState(
+ activityRotation = Surface.ROTATION_0,
+ orientationHandlerRotation = Surface.ROTATION_0
+ )
+
+ override fun getRecentsRotationState() = recentsRotationState
+
+ fun setRecentsRotationState(newValue: RecentsRotationState) {
+ recentsRotationState = newValue
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
index 242bc73..fee4979 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -23,10 +23,12 @@
import com.android.systemui.shared.recents.model.Task
import com.google.common.truth.Truth.assertThat
import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
class FakeTaskIconDataSource : TaskIconDataSource {
- val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
+ val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mockCopyableDrawable() }
+
val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
var shouldLoadSynchronously: Boolean = true
@@ -49,6 +51,17 @@
}
return null
}
+
+ private fun mockCopyableDrawable(): Drawable {
+ val mutableDrawable = mock<Drawable>()
+ val immutableDrawable =
+ mock<Drawable>().apply { whenever(mutate()).thenReturn(mutableDrawable) }
+ val constantState =
+ mock<Drawable.ConstantState>().apply {
+ whenever(newDrawable()).thenReturn(immutableDrawable)
+ }
+ return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) }
+ }
}
fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index 19990a8..ec1da5a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -24,6 +24,7 @@
class FakeTasksRepository : RecentTasksRepository {
private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
+ private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
@@ -37,7 +38,17 @@
override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
visibleTasks.value = visibleTaskIdList
- tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } }
+ tasks.value =
+ tasks.value.map {
+ it.apply {
+ thumbnail = thumbnailDataMap[it.key.id]
+ taskIconDataMap[it.key.id].let { taskIconData ->
+ icon = taskIconData?.icon
+ titleDescription = taskIconData?.contentDescription
+ title = taskIconData?.title
+ }
+ }
+ }
}
fun seedTasks(tasks: List<Task>) {
@@ -47,4 +58,8 @@
fun seedThumbnailData(thumbnailDataMap: Map<Int, ThumbnailData>) {
this.thumbnailDataMap = thumbnailDataMap
}
+
+ fun seedIconData(iconDataMap: Map<Int, TaskIconQueryResponse>) {
+ this.taskIconDataMap = iconDataMap
+ }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
similarity index 84%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
index eff926d..e74fe4b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
@@ -25,12 +25,12 @@
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
-/** Test for [RecentsDeviceProfileRepository] */
+/** Test for [RecentsDeviceProfileRepositoryImpl] */
@RunWith(AndroidJUnit4::class)
-class RecentsDeviceProfileRepositoryTest : FakeInvariantDeviceProfileTest() {
+class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
private val recentsViewContainer = mock<RecentsViewContainer>()
- private val systemUnderTest = RecentsDeviceProfileRepository(recentsViewContainer)
+ private val systemUnderTest = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
@Test
fun deviceProfileMappedCorrectly() {
@@ -39,6 +39,6 @@
whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
assertThat(systemUnderTest.getRecentsDeviceProfile())
- .isEqualTo(RecentsDeviceProfileRepository.RecentsDeviceProfile(isLargeScreen = true))
+ .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, widthPx = 1600, heightPx = 2560))
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt
similarity index 71%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt
index 1f4da26..017f037 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt
@@ -16,26 +16,32 @@
package com.android.quickstep.recents.data
+import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
+import com.android.quickstep.orientation.SeascapePagedViewHandler
import com.android.quickstep.util.RecentsOrientedState
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
-/** Test for [RecentsRotationStateRepository] */
-class RecentsRotationStateRepositoryTest {
+/** Test for [RecentsRotationStateRepositoryImpl] */
+class RecentsRotationStateRepositoryImplTest {
private val recentsOrientedState = mock<RecentsOrientedState>()
- private val systemUnderTest = RecentsRotationStateRepository(recentsOrientedState)
+ private val systemUnderTest = RecentsRotationStateRepositoryImpl(recentsOrientedState)
@Test
fun orientedStateMappedCorrectly() {
whenever(recentsOrientedState.recentsActivityRotation).thenReturn(ROTATION_90)
+ whenever(recentsOrientedState.orientationHandler).thenReturn(SeascapePagedViewHandler())
assertThat(systemUnderTest.getRecentsRotationState())
.isEqualTo(
- RecentsRotationStateRepository.RecentsRotationState(activityRotation = ROTATION_90)
+ RecentsRotationState(
+ activityRotation = ROTATION_90,
+ orientationHandlerRotation = ROTATION_270
+ )
)
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
index e657d59..02f1d11 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -24,9 +24,9 @@
import android.graphics.Rect
import android.view.Surface.ROTATION_90
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
-import com.android.quickstep.recents.data.RecentsRotationStateRepository
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
import com.android.systemui.shared.recents.model.Task
@@ -56,8 +56,8 @@
}
)
- private val deviceProfileRepository = mock<RecentsDeviceProfileRepository>()
- private val rotationStateRepository = mock<RecentsRotationStateRepository>()
+ private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
+ private val rotationStateRepository = FakeRecentsRotationStateRepository()
private val tasksRepository = FakeTasksRepository()
private val previewPositionHelper = mock<PreviewPositionHelper>()
@@ -93,15 +93,18 @@
tasksRepository.setVisibleTasks(listOf(TASK_ID))
val isLargeScreen = true
+ deviceProfileRepository.setRecentsDeviceProfile(
+ deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen)
+ )
val activityRotation = ROTATION_90
+ rotationStateRepository.setRecentsRotationState(
+ rotationStateRepository
+ .getRecentsRotationState()
+ .copy(activityRotation = activityRotation)
+ )
val isRtl = true
val isRotated = true
- whenever(deviceProfileRepository.getRecentsDeviceProfile())
- .thenReturn(RecentsDeviceProfileRepository.RecentsDeviceProfile(isLargeScreen))
- whenever(rotationStateRepository.getRecentsRotationState())
- .thenReturn(RecentsRotationStateRepository.RecentsRotationState(activityRotation))
-
whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt
new file mode 100644
index 0000000..13e8b09
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/GetSplashSizeUseCaseTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
+import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.views.TaskViewType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class GetSplashSizeUseCaseTest {
+ private val taskThumbnailViewData = TaskThumbnailViewData()
+ private val taskViewData = TaskViewData(TaskViewType.SINGLE)
+ private val recentsDeviceProfileRepository = FakeRecentsDeviceProfileRepository()
+ private val systemUnderTest =
+ GetSplashSizeUseCase(taskThumbnailViewData, taskViewData, recentsDeviceProfileRepository)
+
+ @Test
+ fun execute_whenNoScaleRequired_returnsIntrinsicSize() {
+ taskThumbnailViewData.width.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx
+ taskThumbnailViewData.height.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx
+
+ assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(100, 100))
+ }
+
+ @Test
+ fun execute_whenThumbnailViewIsSmallerThanScreen_returnsScaledSize() {
+ taskThumbnailViewData.width.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
+ taskThumbnailViewData.height.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
+
+ assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(50, 50))
+ }
+
+ @Test
+ fun execute_whenThumbnailViewIsSmallerThanScreen_withNonGridScale_returnsScaledSize() {
+ taskThumbnailViewData.width.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
+ taskThumbnailViewData.height.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
+ taskViewData.nonGridScale.value = 2f
+
+ assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(25, 25))
+ }
+
+ @Test
+ fun execute_whenThumbnailViewIsSmallerThanScreen_withThumbnailViewScale_returnsScaledSize() {
+ taskThumbnailViewData.width.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().widthPx / 2
+ taskThumbnailViewData.height.value =
+ recentsDeviceProfileRepository.getRecentsDeviceProfile().heightPx / 2
+ taskThumbnailViewData.scaleX.value = 2f
+ taskThumbnailViewData.scaleY.value = 2f
+
+ assertThat(systemUnderTest.execute(createIcon(100, 100))).isEqualTo(Point(25, 25))
+ }
+
+ private fun createIcon(width: Int, height: Int): Drawable =
+ mock<Drawable>().apply {
+ whenever(intrinsicWidth).thenReturn(width)
+ whenever(intrinsicHeight).thenReturn(height)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
new file mode 100644
index 0000000..e083046
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.view.Surface
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.TaskIconQueryResponse
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class SplashAlphaUseCaseTest {
+ private val recentsViewData = RecentsViewData()
+ private val taskContainerData = TaskContainerData()
+ private val taskThumbnailViewData = TaskThumbnailViewData()
+ private val recentTasksRepository = FakeTasksRepository()
+ private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
+ private val systemUnderTest =
+ SplashAlphaUseCase(
+ recentsViewData,
+ taskContainerData,
+ taskThumbnailViewData,
+ recentTasksRepository,
+ recentsRotationStateRepository
+ )
+
+ @Test
+ fun execute_withNullThumbnail_showsSplash() = runTest {
+ assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_SHOWN)
+ }
+
+ @Test
+ fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest {
+ setupTask(2)
+ taskContainerData.thumbnailSplashProgress.value = 0.7f
+
+ assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f)
+ }
+
+ @Test
+ fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest {
+ setupTask(2)
+
+ assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
+ }
+
+ @Test
+ fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() =
+ runTest {
+ setupTask(2)
+ recentsViewData.thumbnailSplashProgress.value = 0.5f
+ taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
+ taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+ assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
+ }
+
+ @Test
+ fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest {
+ setupTask(2)
+ recentsViewData.thumbnailSplashProgress.value = 0.5f
+ taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
+ taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+ assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+ }
+
+ @Test
+ fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest {
+ setupTask(2, createThumbnailData(rotation = ROTATION_90))
+ recentsViewData.thumbnailSplashProgress.value = 0.5f
+ taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
+ taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+ assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+ }
+
+ @Test
+ fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest {
+ setupTask(2, createThumbnailData(rotation = ROTATION_90))
+ recentsViewData.thumbnailSplashProgress.value = 0.5f
+ taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
+ taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
+
+ assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
+ }
+
+ private val tasks = (0..5).map(::createTaskWithId)
+
+ private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
+ recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
+ val expectedIconData = createIconData("Task $taskId")
+ recentTasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+ recentTasksRepository.seedTasks(tasks)
+ recentTasksRepository.setVisibleTasks(listOf(taskId))
+ }
+
+ private fun createThumbnailData(
+ rotation: Int = Surface.ROTATION_0,
+ width: Int = THUMBNAIL_WIDTH,
+ height: Int = THUMBNAIL_HEIGHT
+ ): ThumbnailData {
+ val bitmap = mock<Bitmap>()
+ whenever(bitmap.width).thenReturn(width)
+ whenever(bitmap.height).thenReturn(height)
+
+ return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+ }
+
+ private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
+
+ private fun createTaskWithId(taskId: Int) =
+ Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+ }
+
+ companion object {
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+
+ const val SPLASH_HIDDEN = 0f
+ const val SPLASH_SHOWN = 1f
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index 754c9d1..877528e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -21,8 +21,12 @@
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.TaskIconQueryResponse
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -30,6 +34,8 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Splash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
@@ -40,8 +46,10 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -53,18 +61,27 @@
private val taskContainerData = TaskContainerData()
private val tasksRepository = FakeTasksRepository()
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+ private val splashAlphaUseCase: SplashAlphaUseCase = mock()
+ private val getSplashSizeUseCase: GetSplashSizeUseCase = mock()
private val systemUnderTest by lazy {
TaskThumbnailViewModel(
recentsViewData,
taskViewData,
taskContainerData,
tasksRepository,
- mGetThumbnailPositionUseCase
+ mGetThumbnailPositionUseCase,
+ splashAlphaUseCase,
+ getSplashSizeUseCase,
)
}
private val tasks = (0..5).map(::createTaskWithId)
+ @Before
+ fun setUp() {
+ whenever(getSplashSizeUseCase.execute(any())).thenReturn(Point())
+ }
+
@Test
fun initialStateIsUninitialized() = runTest {
assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
@@ -147,9 +164,11 @@
}
@Test
- fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest {
- val expectedThumbnailData = createThumbnailData()
+ fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() = runTest {
+ val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+ val expectedIconData = createIconData("Task 2")
+ tasksRepository.seedIconData(mapOf(2 to expectedIconData))
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(listOf(2))
val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
@@ -157,17 +176,23 @@
systemUnderTest.bind(recentTask)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
- Snapshot(
- backgroundColor = Color.rgb(2, 2, 2),
- bitmap = expectedThumbnailData.thumbnail!!,
+ SnapshotSplash(
+ Snapshot(
+ backgroundColor = Color.rgb(2, 2, 2),
+ bitmap = expectedThumbnailData.thumbnail!!,
+ thumbnailRotation = Surface.ROTATION_270,
+ ),
+ Splash(expectedIconData.icon, Point())
)
)
}
@Test
- fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest {
+ fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() = runTest {
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+ val expectedIconData = createIconData("Task 2")
+ tasksRepository.seedIconData(mapOf(2 to expectedIconData))
tasksRepository.seedTasks(tasks)
val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
@@ -177,14 +202,35 @@
tasksRepository.setVisibleTasks(listOf(2))
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
- Snapshot(
- backgroundColor = Color.rgb(2, 2, 2),
- bitmap = expectedThumbnailData.thumbnail!!,
+ SnapshotSplash(
+ Snapshot(
+ backgroundColor = Color.rgb(2, 2, 2),
+ bitmap = expectedThumbnailData.thumbnail!!,
+ thumbnailRotation = Surface.ROTATION_0,
+ ),
+ Splash(expectedIconData.icon, Point())
)
)
}
@Test
+ fun bindStoppedTask_thenStateContainsSplashSizeFromUseCase() = runTest {
+ val expectedSplashSize = Point(100, 150)
+ whenever(getSplashSizeUseCase.execute(any())).thenReturn(expectedSplashSize)
+ val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
+ tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+ val expectedIconData = createIconData("Task 2")
+ tasksRepository.seedIconData(mapOf(2 to expectedIconData))
+ tasksRepository.seedTasks(tasks)
+ tasksRepository.setVisibleTasks(listOf(2))
+ val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+ systemUnderTest.bind(recentTask)
+ val uiState = systemUnderTest.uiState.first() as SnapshotSplash
+ assertThat(uiState.splash.size).isEqualTo(expectedSplashSize)
+ }
+
+ @Test
fun getSnapshotMatrix_MissingThumbnail() = runTest {
val taskId = 2
val recentTask = TaskThumbnail(taskId = taskId, isRunning = false)
@@ -212,19 +258,42 @@
.isEqualTo(MATRIX)
}
+ @Test
+ fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() = runTest {
+ recentsViewData.tintAmount.value = 0.32f
+ taskContainerData.taskMenuOpenProgress.value = 0f
+ assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
+ }
+
+ @Test
+ fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() = runTest {
+ recentsViewData.tintAmount.value = 0f
+ taskContainerData.taskMenuOpenProgress.value = 1f
+ assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
+ }
+
+ @Test
+ fun getForegroundScrimDimProgress_returnsNoScrim() = runTest {
+ recentsViewData.tintAmount.value = 0f
+ taskContainerData.taskMenuOpenProgress.value = 0f
+ assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
+ }
+
private fun createTaskWithId(taskId: Int) =
Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
colorBackground = Color.argb(taskId, taskId, taskId, taskId)
}
- private fun createThumbnailData(): ThumbnailData {
+ private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
val bitmap = mock<Bitmap>()
whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
- return ThumbnailData(thumbnail = bitmap)
+ return ThumbnailData(thumbnail = bitmap, rotation = rotation)
}
+ private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
+
companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt
new file mode 100644
index 0000000..6c526a4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.util.SafeCloseable
+import com.android.quickstep.DeviceConfigWrapper.Companion.configHelper
+import com.android.quickstep.util.DeviceConfigHelper.Companion.prefs
+import java.util.concurrent.CountDownLatch
+import java.util.function.BooleanSupplier
+import org.junit.Assert
+import org.junit.Assume
+
+/** Helper methods for testing */
+object TestExtensions {
+
+ @JvmStatic
+ fun overrideNavConfigFlag(
+ key: String,
+ value: Boolean,
+ targetValue: BooleanSupplier
+ ): AutoCloseable {
+ Assume.assumeTrue(BuildConfig.IS_DEBUG_DEVICE)
+ if (targetValue.asBoolean == value) {
+ return AutoCloseable {}
+ }
+
+ navConfigEditWatcher().let {
+ prefs.edit().putBoolean(key, value).commit()
+ it.close()
+ }
+ Assert.assertEquals(value, targetValue.asBoolean)
+
+ val watcher = navConfigEditWatcher()
+ return AutoCloseable {
+ prefs.edit().remove(key).commit()
+ watcher.close()
+ }
+ }
+
+ private fun navConfigEditWatcher(): SafeCloseable {
+ val wait = CountDownLatch(1)
+ val listener = Runnable { wait.countDown() }
+ configHelper.addChangeListener(listener)
+
+ return SafeCloseable {
+ wait.await()
+ configHelper.removeChangeListener(listener)
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/FloatingMaskView.java b/src/com/android/launcher3/allapps/FloatingMaskView.java
index 606eb03..cee5e18 100644
--- a/src/com/android/launcher3/allapps/FloatingMaskView.java
+++ b/src/com/android/launcher3/allapps/FloatingMaskView.java
@@ -21,6 +21,7 @@
import android.view.ViewGroup;
import android.widget.ImageView;
+import androidx.annotation.VisibleForTesting;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
@@ -53,13 +54,21 @@
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
- ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
- AllAppsRecyclerView allAppsContainerView =
- mActivityContext.getAppsView().getActiveRecyclerView();
+ setParameters((ViewGroup.MarginLayoutParams) getLayoutParams(),
+ mActivityContext.getAppsView().getActiveRecyclerView());
+ }
+
+ @VisibleForTesting
+ void setParameters(ViewGroup.MarginLayoutParams lp, AllAppsRecyclerView recyclerView) {
if (lp != null) {
- lp.rightMargin = allAppsContainerView.getPaddingRight();
- lp.leftMargin = allAppsContainerView.getPaddingLeft();
- mBottomBox.setMinimumHeight(allAppsContainerView.getPaddingBottom());
+ lp.rightMargin = recyclerView.getPaddingRight();
+ lp.leftMargin = recyclerView.getPaddingLeft();
+ getBottomBox().setMinimumHeight(recyclerView.getPaddingBottom());
}
}
+
+ @VisibleForTesting
+ ImageView getBottomBox() {
+ return mBottomBox;
+ }
}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index c1264d6..e215cab 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -403,6 +403,7 @@
mLockText.setHorizontallyScrolling(false);
mPrivateSpaceSettingsButton.setVisibility(
isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE);
+ mPrivateSpaceSettingsButton.setClickable(isPrivateSpaceSettingsAvailable());
}
lockPill.setVisibility(VISIBLE);
lockPill.setOnClickListener(view -> lockingAction(/* lock */ true));
@@ -425,6 +426,7 @@
lockPill.setContentDescription(mLockedStateContentDesc);
mPrivateSpaceSettingsButton.setVisibility(GONE);
+ mPrivateSpaceSettingsButton.setClickable(false);
transitionView.setVisibility(GONE);
}
case STATE_TRANSITION -> {
@@ -660,10 +662,7 @@
return;
}
attachFloatingMaskView(expand);
- PropertySetter headerSetter = new AnimatedPropertySetter();
- headerSetter.add(updateSettingsGearAlpha(expand));
- headerSetter.add(updateLockTextAlpha(expand));
- AnimatorSet animatorSet = headerSetter.buildAnim();
+ AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -708,12 +707,16 @@
}
}));
if (expand) {
- animatorSet.playTogether(animateAlphaOfIcons(true),
+ animatorSet.playTogether(updateSettingsGearAlpha(true),
+ updateLockTextAlpha(true),
+ animateAlphaOfIcons(true),
animatePillTransition(true),
translateFloatingMaskView(false));
} else {
AnimatorSet parallelSet = new AnimatorSet();
- parallelSet.playTogether(animateAlphaOfIcons(false),
+ parallelSet.playTogether(updateSettingsGearAlpha(false),
+ updateLockTextAlpha(false),
+ animateAlphaOfIcons(false),
animatePillTransition(false));
if (isPrivateSpaceHidden()) {
animatorSet.playSequentially(parallelSet,
@@ -794,6 +797,14 @@
@Override
public void onAnimationStart(Animator animator) {
mPrivateSpaceSettingsButton.setVisibility(VISIBLE);
+ mPrivateSpaceSettingsButton.setClickable(false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ if (expand) {
+ mPrivateSpaceSettingsButton.setClickable(true);
+ }
}
});
return settingsAlphaAnim;
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 52ce4e8..bd9298b 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -44,11 +44,13 @@
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback;
+import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceGroup.PreferencePositionCallback;
import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BuildConfig;
+import com.android.launcher3.Flags;
import com.android.launcher3.LauncherFiles;
import com.android.launcher3.R;
import com.android.launcher3.states.RotationHelper;
@@ -165,6 +167,7 @@
private boolean mRestartOnResume = false;
private String mHighLightKey;
+
private boolean mPreferenceHighlighted = false;
@Override
@@ -198,11 +201,62 @@
}
}
+ // If the target preference is not in the current preference screen, find the parent
+ // preference screen that contains the target preference and set it as the preference
+ // screen.
+ if (Flags.navigateToChildPreference()
+ && mHighLightKey != null
+ && !isKeyInPreferenceGroup(mHighLightKey, screen)) {
+ final PreferenceScreen parentPreferenceScreen =
+ findParentPreference(screen, mHighLightKey);
+ if (parentPreferenceScreen != null && getActivity() != null) {
+ if (!TextUtils.isEmpty(parentPreferenceScreen.getTitle())) {
+ getActivity().setTitle(parentPreferenceScreen.getTitle());
+ }
+ setPreferenceScreen(parentPreferenceScreen);
+ return;
+ }
+ }
+
if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) {
getActivity().setTitle(getPreferenceScreen().getTitle());
}
}
+ private boolean isKeyInPreferenceGroup(String targetKey, PreferenceGroup parent) {
+ for (int i = 0; i < parent.getPreferenceCount(); i++) {
+ Preference pref = parent.getPreference(i);
+ if (pref.getKey() != null && pref.getKey().equals(targetKey)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Finds the parent preference screen for the given target key.
+ *
+ * @param parent the parent preference screen
+ * @param targetKey the key of the preference to find
+ * @return the parent preference screen that contains the target preference
+ */
+ @Nullable
+ private PreferenceScreen findParentPreference(PreferenceScreen parent, String targetKey) {
+ for (int i = 0; i < parent.getPreferenceCount(); i++) {
+ Preference pref = parent.getPreference(i);
+ if (pref instanceof PreferenceScreen) {
+ PreferenceScreen foundKey = findParentPreference((PreferenceScreen) pref,
+ targetKey);
+ if (foundKey != null) {
+ return foundKey;
+ }
+ } else if (pref.getKey() != null && pref.getKey().equals(targetKey)) {
+ return parent;
+ }
+ }
+ return null;
+ }
+
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
diff --git a/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt b/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt
new file mode 100644
index 0000000..cf03adc
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/FloatingMaskViewTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps
+
+import android.content.Context
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.ImageView
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+class FloatingMaskViewTest {
+ @Mock
+ private val mockAllAppsRecyclerView: AllAppsRecyclerView? = null
+
+ @Mock
+ private val mockBottomBox: ImageView? = null
+ private var mVut: FloatingMaskView? = null
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ val context: Context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ mVut = FloatingMaskView(context)
+ mVut!!.layoutParams = MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+
+ @Test
+ fun setParameters_paramsMarginEqualRecyclerViewPadding() {
+ val floatingMaskView = Mockito.spy(mVut)
+ Mockito.`when`(mockAllAppsRecyclerView!!.paddingLeft).thenReturn(PADDING_PX)
+ Mockito.`when`(mockAllAppsRecyclerView.paddingRight).thenReturn(PADDING_PX)
+ Mockito.`when`(mockAllAppsRecyclerView.paddingBottom).thenReturn(PADDING_PX)
+ Mockito.`when`(floatingMaskView!!.bottomBox).thenReturn(mockBottomBox)
+ val lp = floatingMaskView.layoutParams as MarginLayoutParams
+
+ floatingMaskView.setParameters(lp, mockAllAppsRecyclerView)
+
+ Truth.assertThat(lp.leftMargin).isEqualTo(PADDING_PX)
+ Truth.assertThat(lp.rightMargin).isEqualTo(PADDING_PX)
+ Mockito.verify(mockBottomBox)?.minimumHeight = PADDING_PX
+ }
+
+ companion object {
+ private const val PADDING_PX = 15
+ }
+}