diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml
index c659331bde..2524c760f4 100644
--- a/go/quickstep/res/values/styles.xml
+++ b/go/quickstep/res/values/styles.xml
@@ -16,7 +16,7 @@
-->
-
\ No newline at end of file
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 0f997f9b77..0bb971e166 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -95,6 +95,6 @@
?androidprv:attr/colorAccentPrimaryVariant
- ?androidprv:attr/materialColorPrimaryFixedDim
- ?androidprv:attr/materialColorOnPrimaryFixed
+ ?attr/materialColorPrimaryFixedDim
+ ?attr/materialColorOnPrimaryFixed
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index d4f66e2ee1..867ce1769d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -449,11 +449,13 @@
32dp
36dp
+ 28dp
24dp
12dp
16dp
6dp
8dp
+ @dimen/bubblebar_icon_spacing
12dp
1dp
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 153bc99e90..1f4720c255 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -124,7 +124,7 @@
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5e5487bb8e..44601b73f1 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -356,6 +356,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
+ // Prepare taskbar for animation synchronization. This needs to happen here before any
+ // app transition is created.
+ LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
+ if (enableScalingRevealHomeAnimation() && taskbarController != null) {
+ taskbarController.setIgnoreInAppFlagForSync(true);
+ onEndCallback.add(() -> taskbarController.setIgnoreInAppFlagForSync(false));
+ }
+
IBinder cookie = mAppLaunchRunner.supportsReturnTransition()
? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null;
addLaunchCookie(cookie, (ItemInfo) v.getTag(), options);
@@ -1924,6 +1932,21 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
anim.addListener(mForceInvisibleListener);
}
+ // Syncs the app launch animation and taskbar stash animation (if exists).
+ if (enableScalingRevealHomeAnimation()) {
+ LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
+ if (taskbarController != null) {
+ taskbarController.setIgnoreInAppFlagForSync(false);
+
+ if (launcherClosing) {
+ Animator taskbar = taskbarController.createAnimToApp();
+ if (taskbar != null) {
+ anim.play(taskbar);
+ }
+ }
+ }
+ }
+
result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy,
skipFirstFrame);
}
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index d0be2f3cd1..0b1863398e 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -47,11 +47,11 @@ import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.WidgetPredictionsRequester;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
-import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
@@ -60,10 +60,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
/** An Activity that can host Launcher's widget picker. */
public class WidgetPickerActivity extends BaseActivity {
@@ -112,7 +110,8 @@ public class WidgetPickerActivity extends BaseActivity {
private WidgetsModel mModel;
private LauncherAppState mApp;
private WidgetPredictionsRequester mWidgetPredictionsRequester;
- private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+ private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {
+ });
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
@@ -287,45 +286,41 @@ public class WidgetPickerActivity extends BaseActivity {
};
}
- /** Updates the model with widgets, applies filters and launches the widgets sheet once
- * widgets are available */
+ /**
+ * Updates the model with widgets, applies filters and launches the widgets sheet once
+ * widgets are available
+ */
private void refreshAndBindWidgets() {
MODEL_EXECUTOR.execute(() -> {
LauncherAppState app = LauncherAppState.getInstance(this);
Context context = app.getContext();
mModel.update(app, null);
- final List allWidgets =
- mModel.getFilteredWidgetsListForPicker(context, mWidgetsFilter);
- final List defaultWidgets =
- shouldShowDefaultWidgets() ? mModel.getFilteredWidgetsListForPicker(context,
- mDefaultWidgetsFilter) : List.of();
- bindWidgets(allWidgets, defaultWidgets);
+ bindWidgets(mModel.getWidgetsByPackageItem());
// Open sheet once widgets are available, so that it doesn't interrupt the open
// animation.
openWidgetsSheet();
if (mUiSurface != null) {
- Map allWidgetItems = allWidgets.stream()
- .filter(entry -> entry instanceof WidgetsListContentEntry)
- .flatMap(entry -> entry.mWidgets.stream())
- .distinct()
- .collect(Collectors.toMap(
- widget -> new ComponentKey(widget.componentName, widget.user),
- Function.identity()
- ));
mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
- mUiSurface, allWidgetItems);
+ mUiSurface, mModel.getWidgetsByComponentKey());
mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
}
});
}
- private void bindWidgets(List allWidgets,
- List defaultWidgets) {
+ private void bindWidgets(Map> widgets) {
+ WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder(
+ mApp.getContext());
+
+ final List allWidgets = builder.build(widgets, mWidgetsFilter);
+ final List defaultWidgets =
+ shouldShowDefaultWidgets() ? builder.build(widgets,
+ mDefaultWidgetsFilter) : List.of();
+
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(allWidgets, defaultWidgets));
}
- private void openWidgetsSheet() {
+ private void openWidgetsSheet() {
MAIN_EXECUTOR.execute(() -> {
mWidgetSheet = WidgetsFullSheet.show(this, true);
mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
@@ -392,7 +387,7 @@ public class WidgetPickerActivity extends BaseActivity {
mActiveOnBackAnimationCallback.onBackCancelled();
mActiveOnBackAnimationCallback = null;
}
- };
+ }
private boolean shouldShowDefaultWidgets() {
// If optional filters such as size filter are present, we display them as default widgets.
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 84c2ed252b..7a8b58e58f 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,12 +31,12 @@ import android.view.View;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.ColorInt;
-import androidx.core.content.ContextCompat;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.FloatingHeaderRow;
import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.util.Themes;
/**
* A view which shows a horizontal divider
@@ -84,10 +84,9 @@ public class AppsDividerView extends View implements FloatingHeaderRow {
getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
};
- mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant);
+ mStrokeColor = Themes.getAttrColor(context, R.attr.materialColorOutlineVariant);
- mAllAppsLabelTextColor = ContextCompat.getColor(context,
- R.color.material_color_on_surface_variant);
+ mAllAppsLabelTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurfaceVariant);
mAccessibilityManager = AccessibilityManager.getInstance(context);
setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 779009a6b0..0add1c41de 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -19,6 +19,7 @@ import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR
import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
+import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP;
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
@@ -256,6 +257,24 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
}
+ /**
+ * Create Taskbar animation to be played alongside the Launcher app launch animation.
+ */
+ public @Nullable Animator createAnimToApp() {
+ TaskbarStashController stashController = mControllers.taskbarStashController;
+ stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true);
+ return stashController.createApplyStateAnimator(stashController.getStashDuration());
+ }
+
+ /**
+ * Temporarily ignore FLAG_IN_APP for app launches to prevent premature taskbar stashing.
+ * This is needed because taskbar gets a signal to stash before we actually start the
+ * app launch animation.
+ */
+ public void setIgnoreInAppFlagForSync(boolean enabled) {
+ mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled);
+ }
+
public void updateTaskbarLauncherStateGoingHome() {
mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, true);
mTaskbarLauncherStateController.applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index 7f9d8a390e..19e987234f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -53,8 +53,7 @@ constructor(
private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
- private val backgroundColor =
- Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright)
+ private val backgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright)
private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 430c0032a6..0c5ad42147 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -95,6 +95,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
public static final int FLAG_STASHED_SYSUI = 1 << 9; // app pinning,...
public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
+ public static final int FLAG_IGNORE_IN_APP = 1 << 12; // used to sync with app launch animation
// If any of these flags are enabled, isInApp should return true.
private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -1263,6 +1264,11 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
*/
@Nullable
public Animator createSetStateAnimator(long flags, long duration) {
+ // We do this when we want to synchronize the app launch and taskbar stash animations.
+ if (hasAnyFlag(FLAG_IGNORE_IN_APP) && hasAnyFlag(flags, FLAG_IN_APP)) {
+ flags = flags & ~FLAG_IN_APP;
+ }
+
boolean isStashed = mStashCondition.test(flags);
if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 83d4df29c8..5be01718e6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -635,8 +635,8 @@ public class BubbleBarController extends IBubblesListener.Stub {
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{
- com.android.internal.R.attr.materialColorOnPrimaryFixed,
- com.android.internal.R.attr.materialColorPrimaryFixed
+ R.attr.materialColorOnPrimaryFixed,
+ R.attr.materialColorPrimaryFixed
});
int overflowIconColor = ta.getColor(0, Color.WHITE);
int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 1c33be624a..2311d42e16 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -31,6 +31,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -62,6 +63,7 @@ public class BubbleBarViewController {
private final TaskbarActivityContext mActivity;
private final BubbleBarView mBarView;
private int mIconSize;
+ private int mBubbleBarPadding;
// Initialized in init.
private BubbleStashController mBubbleStashController;
@@ -110,10 +112,9 @@ public class BubbleBarViewController {
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
-
+ onBubbleBarConfigurationChanged(/* animate= */ false);
mActivity.addOnDeviceProfileChangeListener(
- dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true));
- updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false);
+ dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
mBubbleBarScale.updateValue(1f);
mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
mBubbleBarClickListener = v -> onBubbleBarClicked();
@@ -335,27 +336,60 @@ public class BubbleBarViewController {
// Modifying view related properties.
//
- private void updateBubbleBarIconSize(int newIconSize, boolean animate) {
+ /** Notifies controller of configuration change, so bubble bar can be adjusted */
+ public void onBubbleBarConfigurationChanged(boolean animate) {
+ int newIconSize;
+ int newPadding;
Resources res = mActivity.getResources();
+ if (mBubbleStashController.isBubblesShowingOnHome()) {
+ newIconSize = getBubbleBarIconSizeFromDeviceProfile(res);
+ newPadding = getBubbleBarPaddingFromDeviceProfile(res);
+ } else {
+ // the bubble bar is shown inside the persistent task bar, use preset sizes
+ newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
+ newPadding = res.getDimensionPixelSize(
+ R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+ }
+ updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
+ }
+
+
+ private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
DisplayMetrics dm = res.getDisplayMetrics();
float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
APP_ICON_SMALL_DP, dm);
float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
APP_ICON_MEDIUM_DP, dm);
- float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
- APP_ICON_LARGE_DP, dm);
float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f;
- float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
- mIconSize = newIconSize <= smallMediumThreshold
+ int taskbarIconSize = deviceProfile.taskbarIconSize;
+ return taskbarIconSize <= smallMediumThreshold
? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) :
res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
- float bubbleBarPadding = newIconSize >= mediumLargeThreshold
+
+ }
+
+ private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ DisplayMetrics dm = res.getDisplayMetrics();
+ float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ APP_ICON_MEDIUM_DP, dm);
+ float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ APP_ICON_LARGE_DP, dm);
+ float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
+ return deviceProfile.taskbarIconSize >= mediumLargeThreshold
? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) :
res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+ }
+
+ private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) {
+ if (mIconSize == iconSize && mBubbleBarPadding == padding) return;
+ mIconSize = iconSize;
+ mBubbleBarPadding = padding;
if (animate) {
- mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding);
+ mBarView.animateBubbleBarIconSize(iconSize, padding);
} else {
- mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding);
+ mBarView.setIconSizeAndPadding(iconSize, padding);
}
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index f020c8f170..20eaddc6f9 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1631,14 +1631,17 @@ public abstract class AbsSwipeUpHandler rawTasks =
- mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+ ArrayList rawTasks;
+ try {
+ rawTasks = mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+ } catch (SystemUiProxy.GetRecentTasksException e) {
+ return INVALID_RESULT;
+ }
// The raw tasks are given in most-recent to least-recent order, we need to reverse it
Collections.reverse(rawTasks);
@@ -416,8 +420,12 @@ public class RecentTasksList {
}
writer.println(prefix + " ]");
int currentUserId = Process.myUserHandle().getIdentifier();
- ArrayList rawTasks =
- mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+ ArrayList rawTasks;
+ try {
+ rawTasks = mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+ } catch (SystemUiProxy.GetRecentTasksException e) {
+ rawTasks = new ArrayList<>();
+ }
writer.println(prefix + " rawTasks=[");
for (GroupedRecentTaskInfo task : rawTasks) {
TaskInfo taskInfo1 = task.getTaskInfo1();
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index cd62265751..f902284fcb 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -33,6 +33,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -416,7 +417,8 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, E
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED
| SYSUI_STATE_MAGNIFICATION_OVERLAP
| SYSUI_STATE_DEVICE_DREAMING
- | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+ | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+ | SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 66aa897794..3f73959254 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -1394,10 +1394,26 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
}
}
- public ArrayList getRecentTasks(int numTasks, int userId) {
+ public static class GetRecentTasksException extends Exception {
+ public GetRecentTasksException(String message) {
+ super(message);
+ }
+
+ public GetRecentTasksException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * Retrieves a list of Recent tasks from ActivityManager.
+ * @throws GetRecentTasksException if IRecentTasks is not initialized, or when we get
+ * RemoteException from server side
+ */
+ public ArrayList getRecentTasks(int numTasks, int userId)
+ throws GetRecentTasksException {
if (mRecentTasks == null) {
- Log.w(TAG, "getRecentTasks() failed due to null mRecentTasks");
- return new ArrayList<>();
+ Log.e(TAG, "getRecentTasks() failed due to null mRecentTasks");
+ throw new GetRecentTasksException("null mRecentTasks");
}
try {
final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks,
@@ -1407,8 +1423,8 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
}
return new ArrayList<>(Arrays.asList(rawTasks));
} catch (RemoteException e) {
- Log.w(TAG, "Failed call getRecentTasks", e);
- return new ArrayList<>();
+ Log.e(TAG, "Failed call getRecentTasks", e);
+ throw new GetRecentTasksException("Failed call getRecentTasks", e);
}
}
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 5ac04da0ac..717f6c879b 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -39,7 +39,8 @@ import android.util.ArrayMap;
import android.util.Log;
import android.util.Xml;
-import com.android.launcher3.AutoInstallsLayout;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.logging.InstanceId;
@@ -73,7 +74,6 @@ public class SettingsChangeLogger implements
new MainThreadInitializedObject<>(SettingsChangeLogger::new);
private static final String TAG = "SettingsChangeLogger";
- private static final String ROOT_TAG = "androidx.preference.PreferenceScreen";
private static final String BOOLEAN_PREF = "SwitchPreference";
private final Context mContext;
@@ -85,8 +85,13 @@ public class SettingsChangeLogger implements
private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent;
private SettingsChangeLogger(Context context) {
+ this(context, StatsLogManager.newInstance(context));
+ }
+
+ @VisibleForTesting
+ SettingsChangeLogger(Context context, StatsLogManager statsLogManager) {
mContext = context;
- mStatsLogManager = StatsLogManager.newInstance(mContext);
+ mStatsLogManager = statsLogManager;
mLoggablePrefs = loadPrefKeys(context);
DisplayController.INSTANCE.get(context).addChangeListener(this);
mNavMode = DisplayController.getNavigationMode(context);
@@ -105,7 +110,13 @@ public class SettingsChangeLogger implements
ArrayMap result = new ArrayMap<>();
try {
- AutoInstallsLayout.beginDocument(parser, ROOT_TAG);
+ // Move cursor to first tag because it could be
+ // androidx.preference.PreferenceScreen or PreferenceScreen
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.START_TAG
+ && eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next();
+ }
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG
@@ -189,13 +200,19 @@ public class SettingsChangeLogger implements
prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff));
}
+ @VisibleForTesting
+ ArrayMap getLoggingPrefs() {
+ return mLoggablePrefs;
+ }
+
@Override
public void close() {
getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
}
- private static class LoggablePref {
+ @VisibleForTesting
+ static class LoggablePref {
public boolean defaultValue;
public int eventIdOn;
public int eventIdOff;
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
new file mode 100644
index 0000000000..7a5a71481c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.di
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.TasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.android.quickstep.task.viewmodel.TaskViewData
+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
+
+class RecentsDependencies private constructor(private val appContext: Context) {
+ private val scopes = mutableMapOf()
+
+ init {
+ startDefaultScope(appContext)
+ }
+
+ /**
+ * This function initialised the default scope with RecentsView dependencies. These dependencies
+ * are used multiple times and should be a singleton to share across Recents classes.
+ */
+ private fun startDefaultScope(appContext: Context) {
+ createScope(DEFAULT_SCOPE_ID).apply {
+ set(RecentsViewData::class.java.simpleName, RecentsViewData())
+
+ // Create RecentsTaskRepository singleton
+ val recentTasksRepository: RecentTasksRepository =
+ with(RecentsModel.INSTANCE.get(appContext)) {
+ TasksRepository(this, thumbnailCache, iconCache)
+ }
+ set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
+ }
+ }
+
+ inline fun inject(
+ scopeId: RecentsScopeId = "",
+ extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+ ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory)
+
+ @Suppress("UNCHECKED_CAST")
+ @JvmOverloads
+ fun inject(
+ modelClass: Class,
+ scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+ extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+ factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+ ): T {
+ val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID }
+ val scope = scopes[currentScopeId] ?: createScope(currentScopeId)
+
+ log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO)
+ var instance: T?
+ synchronized(this) {
+ instance = getDependency(scope, modelClass)
+ log("found instance? $instance", Log.INFO)
+ if (instance == null) {
+ instance =
+ factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
+ scope[modelClass.simpleName] = instance!!
+ }
+ }
+ return instance!!
+ }
+
+ inline fun provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T =
+ provide(T::class.java, scopeId = scopeId, factory = factory)
+
+ @JvmOverloads
+ fun provide(
+ modelClass: Class,
+ scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+ factory: () -> T,
+ ) = inject(modelClass, scopeId, factory = { factory.invoke() })
+
+ private fun getDependency(scope: RecentsDependenciesScope, modelClass: Class): T? {
+ var instance: T? = scope[modelClass.simpleName] as T?
+ if (instance == null) {
+ instance =
+ scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId ->
+ getScope(scopeId)[modelClass.simpleName]
+ } as T?
+ }
+ if (instance != null) log("Found dependency: $instance", Log.INFO)
+ return instance
+ }
+
+ fun getScope(scope: Any): RecentsDependenciesScope {
+ val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+ return getScope(scopeId)
+ }
+
+ fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
+ scopes[scopeId] ?: createScope(scopeId)
+
+ // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
+ // Each class should be responsible for providing a factory function to create a new instance.
+ @Suppress("UNCHECKED_CAST")
+ private fun createDependency(
+ modelClass: Class,
+ scopeId: RecentsScopeId,
+ extras: RecentsDependenciesExtras,
+ ): T {
+ log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN)
+ val instance: Any =
+ when (modelClass) {
+ RecentTasksRepository::class.java -> {
+ with(RecentsModel.INSTANCE.get(appContext)) {
+ TasksRepository(this, thumbnailCache, iconCache)
+ }
+ }
+ RecentsViewData::class.java -> RecentsViewData()
+ TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras))
+ TaskViewData::class.java -> {
+ val taskViewType = extras["TaskViewType"] as TaskViewType
+ TaskViewData(taskViewType)
+ }
+ TaskContainerData::class.java -> TaskContainerData()
+ TaskThumbnailViewModel::class.java ->
+ TaskThumbnailViewModel(
+ recentsViewData = inject(),
+ taskViewData = inject(scopeId, extras),
+ taskContainerData = inject(),
+ getThumbnailPositionUseCase = inject(),
+ tasksRepository = inject()
+ )
+ TaskOverlayViewModel::class.java -> {
+ val task = extras["Task"] as Task
+ TaskOverlayViewModel(
+ task = task,
+ recentsViewData = inject(),
+ recentTasksRepository = inject(),
+ getThumbnailPositionUseCase = inject()
+ )
+ }
+ GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
+ GetThumbnailPositionUseCase::class.java ->
+ GetThumbnailPositionUseCase(
+ deviceProfileRepository = inject(),
+ rotationStateRepository = inject(),
+ tasksRepository = inject()
+ )
+ else -> {
+ log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
+ error("Factory for ${modelClass.simpleName} not defined!")
+ }
+ }
+ return instance as T
+ }
+
+ private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
+ return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it }
+ }
+
+ private fun log(message: String, @Log.Level level: Int = Log.DEBUG) {
+ if (DEBUG) {
+ when (level) {
+ Log.WARN -> Log.w(TAG, message)
+ Log.VERBOSE -> Log.v(TAG, message)
+ Log.INFO -> Log.i(TAG, message)
+ Log.ERROR -> Log.e(TAG, message)
+ else -> Log.d(TAG, message)
+ }
+ }
+ }
+
+ companion object {
+ private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope"
+ private const val TAG = "RecentsDependencies"
+ private const val DEBUG = false
+
+ @Volatile private lateinit var instance: RecentsDependencies
+
+ fun initialize(view: View): RecentsDependencies = initialize(view.context)
+
+ fun initialize(context: Context): RecentsDependencies {
+ synchronized(this) {
+ if (!Companion::instance.isInitialized) {
+ instance = RecentsDependencies(context.applicationContext)
+ }
+ }
+ return instance
+ }
+
+ fun getInstance(): RecentsDependencies {
+ if (!Companion::instance.isInitialized) {
+ throw UninitializedPropertyAccessException(
+ "Recents dependencies are not initialized. " +
+ "Call `RecentsDependencies.initialize` before using this container."
+ )
+ }
+ return instance
+ }
+
+ fun destroy() {
+ instance.scopes.clear()
+ instance.startDefaultScope(instance.appContext)
+ }
+ }
+}
+
+inline fun RecentsDependencies.Companion.inject(
+ scope: Any = "",
+ vararg extras: Pair,
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): Lazy = lazy { get(scope, RecentsDependenciesExtras(extras), factory) }
+
+inline fun RecentsDependencies.Companion.get(
+ scope: Any = "",
+ extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T {
+ val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+ return getInstance().inject(scopeId, extras, factory)
+}
+
+inline fun RecentsDependencies.Companion.get(
+ scope: Any = "",
+ vararg extras: Pair,
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T = get(scope, RecentsDependenciesExtras(extras), factory)
+
+fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId)
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
new file mode 100644
index 0000000000..753cb6e7c9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.di
+
+data class RecentsDependenciesExtras(private val data: MutableMap = mutableMapOf()) {
+ constructor(value: Array>) : this(value.toMap().toMutableMap())
+
+ operator fun get(key: String) = data[key]
+
+ operator fun set(key: String, value: Any) {
+ data[key] = value
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
new file mode 100644
index 0000000000..56bb1edfe9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.di
+
+import android.util.Log
+
+class RecentsDependenciesScope(
+ val scopeId: RecentsScopeId,
+ private val dependencies: MutableMap = mutableMapOf(),
+ private val scopeIds: MutableList = mutableListOf()
+) {
+ val scopeIdsLinked: List
+ get() = scopeIds.toList()
+
+ operator fun get(identifier: String): Any? {
+ log("get $identifier")
+ return dependencies[identifier]
+ }
+
+ operator fun set(key: String, value: Any) {
+ synchronized(this) {
+ log("set $key")
+ dependencies[key] = value
+ }
+ }
+
+ fun remove(key: String): Any? {
+ synchronized(this) {
+ log("remove $key")
+ return dependencies.remove(key)
+ }
+ }
+
+ fun linkTo(scope: RecentsDependenciesScope) {
+ log("linking to ${scope.scopeId}")
+ scopeIds += scope.scopeId
+ }
+
+ fun close() {
+ log("reset")
+ synchronized(this) { dependencies.clear() }
+ }
+
+ private fun log(message: String) {
+ if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message")
+ }
+
+ override fun toString(): String =
+ "scopeId: $scopeId" +
+ "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" +
+ "\n linked to: ${scopeIds.joinToString(", ")}"
+
+ private companion object {
+ private const val TAG = "RecentsDependenciesScope"
+ private const val DEBUG = false
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
new file mode 100644
index 0000000000..8b03a84362
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.viewmodel
+
+import com.android.quickstep.recents.data.RecentTasksRepository
+
+class RecentsViewModel(
+ private val recentsTasksRepository: RecentTasksRepository,
+ private val recentsViewData: RecentsViewData
+) {
+ fun refreshAllTaskData() {
+ recentsTasksRepository.getAllTaskData(true)
+ }
+
+ fun updateVisibleTasks(visibleTaskIdList: List) {
+ recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
+ }
+
+ fun updateScale(scale: Float) {
+ recentsViewData.scale.value = scale
+ }
+
+ fun updateFullscreenProgress(fullscreenProgress: Float) {
+ recentsViewData.fullscreenProgress.value = fullscreenProgress
+ }
+
+ fun updateTasksFullyVisible(taskIds: Set) {
+ recentsViewData.settledFullyVisibleTaskIds.value = taskIds
+ }
+
+ fun setOverlayEnabled(isOverlayEnabled: Boolean) {
+ recentsViewData.overlayEnabled.value = isOverlayEnabled
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index d1be3fbf56..fcc2af36b4 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -31,15 +31,14 @@ import androidx.core.view.isVisible
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.ViewPool
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.inject
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.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.util.TaskCornerRadius
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView
import com.android.systemui.shared.system.QuickStepContract
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -50,25 +49,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
- // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
- // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
- // This is using a lazy for now because the dependencies cannot be obtained without DI.
- val viewModel by lazy {
- val recentsView =
- RecentsViewContainer.containerFromContext(context)
- .getOverviewPanel>()
- TaskThumbnailViewModel(
- recentsView.mRecentsViewData!!,
- (parent as TaskView).taskViewData,
- (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
- recentsView.mTasksRepository!!,
- GetThumbnailPositionUseCase(
- recentsView.mDeviceProfileRepository!!,
- recentsView.mOrientedStateRepository!!,
- recentsView.mTasksRepository!!
- )
- )
- }
+
+ private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
private lateinit var viewAttachedScope: CoroutineScope
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index cc53be94ad..9253dbf945 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -18,13 +18,12 @@ package com.android.quickstep.task.util
import android.util.Log
import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
import com.android.quickstep.task.thumbnail.TaskOverlayUiState
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -42,24 +41,12 @@ class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOver
private lateinit var overlayInitializedScope: CoroutineScope
private var uiState: TaskOverlayUiState = Disabled
- // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
- // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
- // This is using a lazy for now because the dependencies cannot be obtained without DI.
- private val viewModel by lazy {
- val recentsView =
- RecentsViewContainer.containerFromContext(
- overlay.taskView.context
- )
- .getOverviewPanel>()
+ private val viewModel: TaskOverlayViewModel by lazy {
TaskOverlayViewModel(
- task,
- recentsView.mRecentsViewData!!,
- recentsView.mTasksRepository!!,
- GetThumbnailPositionUseCase(
- recentsView.mDeviceProfileRepository!!,
- recentsView.mOrientedStateRepository!!,
- recentsView.mTasksRepository
- )
+ task = task,
+ recentsViewData = RecentsDependencies.get(),
+ getThumbnailPositionUseCase = RecentsDependencies.get(),
+ recentTasksRepository = RecentsDependencies.get()
)
}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index ca3bbb463f..4e13d1ce26 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -34,14 +34,14 @@ import kotlinx.coroutines.runBlocking
class TaskOverlayViewModel(
private val task: Task,
recentsViewData: RecentsViewData,
- tasksRepository: RecentTasksRepository,
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+ recentTasksRepository: RecentTasksRepository,
) {
val overlayState =
combine(
recentsViewData.overlayEnabled,
recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
- tasksRepository.getThumbnailById(task.key.id)
+ recentTasksRepository.getThumbnailById(task.key.id)
) { isOverlayEnabled, isFullyVisible, thumbnailData ->
if (isOverlayEnabled && isFullyVisible) {
Enabled(
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
similarity index 96%
rename from quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
rename to quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index e03e1142d7..64656452e7 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.quickstep.task.thumbnail
+package com.android.quickstep.task.viewmodel
import android.annotation.ColorInt
import android.graphics.Matrix
@@ -23,12 +23,12 @@ 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.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.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
new file mode 100644
index 0000000000..ec75d59c34
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() {
+ fun updateScale(scale: Float) {
+ taskViewData.scale.value = scale
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 1db1447303..a3d635914e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -191,10 +191,12 @@ import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.ViewUtils;
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.RecentsRotationStateRepository;
-import com.android.quickstep.recents.data.TasksRepository;
+import com.android.quickstep.recents.di.RecentsDependencies;
import com.android.quickstep.recents.viewmodel.RecentsViewData;
+import com.android.quickstep.recents.viewmodel.RecentsViewModel;
import com.android.quickstep.util.ActiveGestureErrorDetector;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AnimUtils;
@@ -241,8 +243,9 @@ import java.util.stream.Collectors;
/**
* A list of recent tasks.
+ *
* @param : the container that should host recents view
- * @param : the type of base state that will be used
+ * @param : the type of base state that will be used
*/
public abstract class RecentsView mSizeStrategy;
@Nullable
@@ -726,10 +720,12 @@ public abstract class RecentsView new RecentsRotationStateRepository(mOrientationState));
+
+ recentsDependencies.provide(RecentsDeviceProfileRepository.class,
+ () -> new RecentsDeviceProfileRepository(mContainer));
+ } else {
+ mRecentsViewModel = null;
+ }
+
mScrollHapticMinGapMillis = getResources()
.getInteger(R.integer.recentsScrollHapticMinGapMillis);
mFastFlingVelocity = getResources()
.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
mModel = RecentsModel.INSTANCE.get(context);
mIdp = InvariantDeviceProfile.INSTANCE.get(context);
- if (enableRefactorTaskThumbnail()) {
- mTasksRepository = new TasksRepository(
- mModel, mModel.getThumbnailCache(), mModel.getIconCache());
- mOrientedStateRepository = new RecentsRotationStateRepository(mOrientationState);
- mDeviceProfileRepository = new RecentsDeviceProfileRepository(mContainer);
- } else {
- mTasksRepository = null;
- mOrientedStateRepository = null;
- mDeviceProfileRepository = null;
- }
mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
.inflate(R.layout.overview_clear_all_button, this, false);
@@ -1077,6 +1084,7 @@ public abstract class RecentsView= screenEnd - mPageSpacing;
@@ -4651,6 +4671,7 @@ public abstract class RecentsView{
+ builder.addOnFrameCallback(() -> {
// TODO(b/334826842): Handle splash icon for new TTV.
if (!enableRefactorTaskThumbnail()) {
taskContainer.getThumbnailViewDeprecated().refreshSplashView();
@@ -4896,17 +4917,21 @@ public abstract class RecentsView {
// Once we pass a certain threshold, update the sysui flags to match the target
@@ -5390,7 +5415,7 @@ public abstract class RecentsView 0
- ? (int) Utilities.mapRange(
- mAdjacentPageHorizontalOffset,
- getOverScrollShift(),
- getUndampedOverScrollShift())
- : getOverScrollShift();
+ ? (int) Utilities.mapRange(
+ mAdjacentPageHorizontalOffset,
+ getOverScrollShift(),
+ getUndampedOverScrollShift())
+ : getOverScrollShift();
return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
+ overScrollShift + getOffsetFromScrollPosition(pageIndex);
}
@@ -5942,7 +5968,10 @@ public abstract class RecentsView = taskOverlayFactory.createOverlay(this)
- val taskContainerData = TaskContainerData()
-
- private val getThumbnailUseCase by lazy {
- // TODO(b/335649589): Ideally create and obtain this from DI.
- val recentsView =
- RecentsViewContainer.containerFromContext(
- overlay.taskView.context
- )
- .getOverviewPanel>()
- GetThumbnailUseCase(recentsView.mTasksRepository!!)
- }
+ lateinit var taskContainerData: TaskContainerData
+ private val getThumbnailUseCase: GetThumbnailUseCase by RecentsDependencies.inject()
+ private val taskThumbnailViewModel: TaskThumbnailViewModel by
+ RecentsDependencies.inject(snapshotView)
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
+ taskContainerData = RecentsDependencies.get(this)
+ RecentsDependencies.getScope(snapshotView).apply {
+ val taskViewScope = RecentsDependencies.getScope(taskView)
+ linkTo(taskViewScope)
+
+ val taskContainerScope = RecentsDependencies.getScope(this)
+ linkTo(taskContainerScope)
+ }
} else {
require(snapshotView is TaskThumbnailViewDeprecated)
}
@@ -146,12 +152,10 @@ class TaskContainer(
overlay.destroy()
}
- // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
- // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
fun bindThumbnailView() {
// TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
// this should be decided inside TaskThumbnailViewModel.
- thumbnailView.viewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
+ taskThumbnailViewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
}
fun setOverlayEnabled(enabled: Boolean) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 004003c014..f2f036a874 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -80,8 +80,10 @@ import com.android.quickstep.TaskAnimationManager
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskViewUtils
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.BorderAnimator
@@ -115,7 +117,8 @@ constructor(
@IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
annotation class TaskDataChanges
- val taskViewData = TaskViewData(type)
+ private lateinit var taskViewModel: TaskViewModel
+
val taskIds: IntArray
/** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
get() = taskContainers.map { it.task.key.id }.toIntArray()
@@ -441,6 +444,11 @@ constructor(
init {
setOnClickListener { _ -> onClick() }
+
+ if (enableRefactorTaskThumbnail()) {
+ taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
+ }
+
val keyboardFocusHighlightEnabled =
(ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline())
val cursorHoverStatesEnabled = enableCursorHoverStates()
@@ -638,6 +646,7 @@ constructor(
orientedState: RecentsOrientedState,
taskOverlayFactory: TaskOverlayFactory
) {
+
cancelPendingLoadTasks()
taskContainers =
listOf(
@@ -1404,7 +1413,7 @@ constructor(
scaleX = scale
scaleY = scale
if (enableRefactorTaskThumbnail()) {
- taskViewData.scale.value = scale
+ taskViewModel.updateScale(scale)
}
updateSnapshotRadius()
}
@@ -1483,8 +1492,6 @@ constructor(
/** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */
fun notifyIsRunningTaskUpdated() {
- // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
- // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
taskContainers.forEach { it.bindThumbnailView() }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
new file mode 100644
index 0000000000..070eeafc25
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.logging
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
+import com.android.launcher3.logging.InstanceId
+import com.android.launcher3.logging.StatsLogManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class SettingsChangeLoggerTest {
+ private val mContext: Context = ApplicationProvider.getApplicationContext()
+
+ private val mInstanceId = InstanceId.fakeInstanceId(1)
+
+ private lateinit var mSystemUnderTest: SettingsChangeLogger
+
+ @Mock private lateinit var mStatsLogManager: StatsLogManager
+
+ @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
+
+ @Captor private lateinit var mEventCaptor: ArgumentCaptor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
+ whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
+
+ mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+ }
+
+ @After
+ fun tearDown() {
+ mSystemUnderTest.close()
+ }
+
+ @Test
+ fun loggingPrefs_correctDefaultValue() {
+ assertThat(mSystemUnderTest.loggingPrefs["pref_allowRotation"]!!.defaultValue).isFalse()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_add_icon_to_home"]!!.defaultValue).isTrue()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_overview_action_suggestions"]!!.defaultValue)
+ .isTrue()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_smartspace_home_screen"]!!.defaultValue)
+ .isTrue()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_enable_minus_one"]!!.defaultValue).isTrue()
+ }
+
+ @Test
+ fun logSnapshot_defaultValue() {
+ mSystemUnderTest.logSnapshot(mInstanceId)
+
+ verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+ val capturedEvents = mEventCaptor.allValues
+ assertThat(capturedEvents.isNotEmpty()).isTrue()
+ verifyDefaultEvent(capturedEvents)
+ // pref_allowRotation false
+ assertThat(capturedEvents.any { it.id == 616 }).isTrue()
+ }
+
+ @Test
+ fun logSnapshot_updateValue() {
+ LauncherPrefs.get(mContext)
+ .put(
+ item =
+ backedUpItem(
+ sharedPrefKey = "pref_allowRotation",
+ defaultValue = false,
+ ),
+ value = true
+ )
+
+ mSystemUnderTest.logSnapshot(mInstanceId)
+
+ verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+ val capturedEvents = mEventCaptor.allValues
+ assertThat(capturedEvents.isNotEmpty()).isTrue()
+ verifyDefaultEvent(capturedEvents)
+ // pref_allowRotation true
+ assertThat(capturedEvents.any { it.id == 615 }).isTrue()
+ }
+
+ private fun verifyDefaultEvent(capturedEvents: MutableList) {
+ // LAUNCHER_NOTIFICATION_DOT_ENABLED
+ assertThat(capturedEvents.any { it.id == 611 }).isTrue()
+ // LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
+ assertThat(capturedEvents.any { it.id == 625 }).isTrue()
+ // LAUNCHER_THEMED_ICON_DISABLED
+ assertThat(capturedEvents.any { it.id == 837 }).isTrue()
+ // pref_add_icon_to_home true
+ assertThat(capturedEvents.any { it.id == 613 }).isTrue()
+ // pref_overview_action_suggestions true
+ assertThat(capturedEvents.any { it.id == 619 }).isTrue()
+ // pref_smartspace_home_screen true
+ assertThat(capturedEvents.any { it.id == 621 }).isTrue()
+ // pref_enable_minus_one true
+ assertThat(capturedEvents.any { it.id == 617 }).isTrue()
+ }
+}
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 fddc740590..754c9d1e73 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
@@ -32,6 +32,7 @@ import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index 64937b4c15..d0887df9bf 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -59,7 +59,7 @@ class TaskOverlayViewModelTest {
private val tasksRepository = FakeTasksRepository()
private val mGetThumbnailPositionUseCase = mock()
private val systemUnderTest =
- TaskOverlayViewModel(task, recentsViewData, tasksRepository, mGetThumbnailPositionUseCase)
+ TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository)
@Test
fun initialStateIsDisabled() = runTest {
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 5d00255882..d049fbc192 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -16,6 +16,8 @@
package com.android.quickstep;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.TestCase.assertNull;
import static org.junit.Assert.assertEquals;
@@ -69,14 +71,14 @@ public class RecentTasksListTest {
}
@Test
- public void onRecentTasksChanged_doesNotFetchTasks() {
+ public void onRecentTasksChanged_doesNotFetchTasks() throws Exception {
mRecentTasksList.onRecentTasksChanged();
verify(mockSystemUiProxy, times(0))
.getRecentTasks(anyInt(), anyInt());
}
@Test
- public void loadTasksInBackground_onlyKeys_noValidTaskDescription() {
+ public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception {
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
@@ -91,7 +93,19 @@ public class RecentTasksListTest {
}
@Test
- public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() {
+ public void loadTasksInBackground_GetRecentTasksException() throws Exception {
+ when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ .thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed"));
+
+ RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground(
+ Integer.MAX_VALUE, -1, false);
+
+ assertThat(taskList.mRequestId).isEqualTo(-1);
+ assertThat(taskList).isEmpty();
+ }
+
+ @Test
+ public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception {
String taskDescription = "Wheeee!";
ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo();
task1.taskDescription = new ActivityManager.TaskDescription(taskDescription);
@@ -111,7 +125,7 @@ public class RecentTasksListTest {
}
@Test
- public void loadTasksInBackground_freeformTask_createsDesktopTask() {
+ public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception {
ActivityManager.RecentTaskInfo[] tasks = {
createRecentTaskInfo(1 /* taskId */),
createRecentTaskInfo(4 /* taskId */),
@@ -134,7 +148,8 @@ public class RecentTasksListTest {
}
@Test
- public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() {
+ public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask()
+ throws Exception {
ActivityManager.RecentTaskInfo[] tasks = {
createRecentTaskInfo(1 /* taskId */),
createRecentTaskInfo(4 /* taskId */),
diff --git a/res/color-night-v31/material_color_surface.xml b/res/color-night-v31/material_color_surface.xml
deleted file mode 100644
index a645f24dd9..0000000000
--- a/res/color-night-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_bright.xml b/res/color-night-v31/material_color_surface_bright.xml
deleted file mode 100644
index f34ed6c548..0000000000
--- a/res/color-night-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container.xml b/res/color-night-v31/material_color_surface_container.xml
deleted file mode 100644
index 002b88eba4..0000000000
--- a/res/color-night-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_high.xml b/res/color-night-v31/material_color_surface_container_high.xml
deleted file mode 100644
index edd36fcd23..0000000000
--- a/res/color-night-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_highest.xml b/res/color-night-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e54f95380e..0000000000
--- a/res/color-night-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_low.xml b/res/color-night-v31/material_color_surface_container_low.xml
deleted file mode 100644
index 40f0d4c9de..0000000000
--- a/res/color-night-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml
deleted file mode 100644
index 24f559b494..0000000000
--- a/res/color-night-v31/material_color_surface_container_lowest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_dim.xml b/res/color-night-v31/material_color_surface_dim.xml
deleted file mode 100644
index a645f24dd9..0000000000
--- a/res/color-night-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_inverse.xml b/res/color-night-v31/material_color_surface_inverse.xml
deleted file mode 100644
index ac63072a9e..0000000000
--- a/res/color-night-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_variant.xml b/res/color-night-v31/material_color_surface_variant.xml
deleted file mode 100644
index a645f24dd9..0000000000
--- a/res/color-night-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml
index 83822a62bc..28995e32bd 100644
--- a/res/color-night-v31/popup_shade_first.xml
+++ b/res/color-night-v31/popup_shade_first.xml
@@ -12,7 +12,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+
+
diff --git a/res/color-v31/material_color_surface.xml b/res/color-v31/material_color_surface.xml
deleted file mode 100644
index b049851ff4..0000000000
--- a/res/color-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_bright.xml b/res/color-v31/material_color_surface_bright.xml
deleted file mode 100644
index b049851ff4..0000000000
--- a/res/color-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container.xml b/res/color-v31/material_color_surface_container.xml
deleted file mode 100644
index b031c081a9..0000000000
--- a/res/color-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_high.xml b/res/color-v31/material_color_surface_container_high.xml
deleted file mode 100644
index a996d51eba..0000000000
--- a/res/color-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_highest.xml b/res/color-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e7a535af53..0000000000
--- a/res/color-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_low.xml b/res/color-v31/material_color_surface_container_low.xml
deleted file mode 100644
index b8fe01e484..0000000000
--- a/res/color-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml
deleted file mode 100644
index 25e8666862..0000000000
--- a/res/color-v31/material_color_surface_container_lowest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_dim.xml b/res/color-v31/material_color_surface_dim.xml
deleted file mode 100644
index e2d226fa89..0000000000
--- a/res/color-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_inverse.xml b/res/color-v31/material_color_surface_inverse.xml
deleted file mode 100644
index e189862856..0000000000
--- a/res/color-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_variant.xml b/res/color-v31/material_color_surface_variant.xml
deleted file mode 100644
index e2d226fa89..0000000000
--- a/res/color-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml
index 1278bb494a..be73698c2e 100644
--- a/res/color-v31/popup_shade_first.xml
+++ b/res/color-v31/popup_shade_first.xml
@@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+
+
diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml
index 1dd8da60c7..0b317bd395 100644
--- a/res/color/overview_button.xml
+++ b/res/color/overview_button.xml
@@ -1,12 +1,11 @@
-
+
\ No newline at end of file
diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml
index 1278bb494a..be73698c2e 100644
--- a/res/color/popup_shade_first.xml
+++ b/res/color/popup_shade_first.xml
@@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+
+
diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml
index e279fa051a..be4765a45b 100644
--- a/res/drawable/add_item_dialog_background.xml
+++ b/res/drawable/add_item_dialog_background.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
index 1e7cff2e93..62927afeb7 100644
--- a/res/drawable/all_apps_tabs_background.xml
+++ b/res/drawable/all_apps_tabs_background.xml
@@ -30,7 +30,7 @@
android:state_selected="false">
-
+
@@ -39,7 +39,7 @@
android:state_selected="true">
-
+
diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
index 379e0e53d6..a19465dba1 100644
--- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
+++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
@@ -15,8 +15,7 @@
-->
-
+
diff --git a/res/drawable/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml
index f5b68866cb..13959f6925 100644
--- a/res/drawable/button_top_rounded_bordered_ripple.xml
+++ b/res/drawable/button_top_rounded_bordered_ripple.xml
@@ -25,7 +25,7 @@
android:topRightRadius="12dp"
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp" />
-
+
diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml
index f336eea897..e4053e3ba7 100644
--- a/res/drawable/ic_close_work_edu.xml
+++ b/res/drawable/ic_close_work_edu.xml
@@ -20,6 +20,6 @@
android:viewportWidth="960"
android:viewportHeight="960">
diff --git a/res/drawable/ic_private_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml
index cb37c9add5..cc73f6dafe 100644
--- a/res/drawable/ic_private_space_with_background.xml
+++ b/res/drawable/ic_private_space_with_background.xml
@@ -13,14 +13,13 @@
limitations under the License.
-->
+ android:fillColor="?attr/materialColorSurfaceContainerLowest" />
+ android:centerColor="?attr/materialColorSurfaceBright"
+ android:endColor="?attr/materialColorSurfaceBright" />
\ No newline at end of file
diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml
index cfec2b126d..12c4a82f39 100644
--- a/res/drawable/private_space_install_app_icon.xml
+++ b/res/drawable/private_space_install_app_icon.xml
@@ -23,9 +23,9 @@
android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z" />
+ android:fillColor="?attr/materialColorSurfaceContainerLowest" />
+ android:fillColor="?attr/materialColorOnSurface" />
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index e283d3fee3..ddd3042cb9 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -7,7 +7,7 @@
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
- ~ Unless required by applicable law or agreed to in writing, software
+ ~ Unless required by applicable law or agreed to in writing, soft]ware
~ 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
@@ -16,13 +16,12 @@
-
+
+ android:color="?attr/materialColorSurfaceContainerLow" />
diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml
index 9bf2b8d0fc..01ec947158 100644
--- a/res/drawable/work_card.xml
+++ b/res/drawable/work_card.xml
@@ -17,7 +17,7 @@
-
+
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index 52180cff77..e68f0f3d33 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -61,7 +61,7 @@
android:layout_marginBottom="@dimen/ps_lock_icon_margin_bottom"
android:importantForAccessibility="no"
android:src="@drawable/ic_lock"
- app:tint="@color/material_color_primary_fixed_dim"
+ app:tint="?attr/materialColorPrimaryFixedDim"
android:scaleType="center"/>
@android:color/system_accent1_900
-
- @android:color/system_accent2_700
- @android:color/system_accent3_700
- @android:color/system_accent1_700
- @android:color/system_accent2_100
- @android:color/system_accent3_100
- @android:color/system_accent1_100
- @android:color/system_accent2_200
- #FFDAD5
- @android:color/system_accent2_900
- @android:color/system_neutral1_900
- @android:color/system_accent3_200
- @android:color/system_accent3_900
- @android:color/system_accent1_200
- @android:color/system_accent2_700
- #930001
- @android:color/system_accent1_900
- @android:color/system_accent1_600
- @android:color/system_accent2_100
- @android:color/system_accent3_700
- @android:color/system_accent3_100
- @android:color/system_accent1_700
- @android:color/system_neutral1_800
- @android:color/system_accent1_100
- @android:color/system_accent2_800
- @android:color/system_accent3_800
- #690001
- @android:color/system_neutral2_200
- @android:color/system_neutral2_400
- @android:color/system_neutral2_700
- @android:color/system_accent1_800
- @android:color/system_neutral1_100
- @android:color/system_accent1_200
- @android:color/system_accent2_200
- @android:color/system_accent3_200
\ No newline at end of file
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
deleted file mode 100644
index 95b3a630ca..0000000000
--- a/res/values-night/colors.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
- #3F4759
- #583E5B
- #0D0E11
- #2B4678
- #DBE2F9
- #FBD7FC
- #1B1B1F
- #D8E2FF
- #BFC6DC
- #FFDAD5
- #141B2C
- #1B1B1F
- #DEBCDF
- #29132D
- #ADC6FF
- #3F4759
- #930001
- #001A41
- #445E91
- #DBE2F9
- #FAF9FD
- #44474F
- #583E5B
- #FBD7FC
- #2B4678
- #E3E2E6
- #D8E2FF
- #293041
- #402843
- #121316
- #38393C
- #690001
- #121316
- #292A2D
- #343538
- #C4C6D0
- #72747D
- #444746
- #102F60
- #E3E2E6
- #1F1F23
- #ADC6FF
- #BFC6DC
- #DEBCDF
-
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 9d50d07d53..06f0eee50f 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -26,7 +26,56 @@
- true
+
+
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index 727036680d..5c81d496f7 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -110,39 +110,4 @@
@android:color/system_accent1_900
@android:color/system_neutral1_1000
-
- @android:color/system_accent2_700
- @android:color/system_accent3_700
- @android:color/system_accent1_700
- @android:color/system_accent2_900
- @android:color/system_accent3_900
- @android:color/system_accent1_900
- @android:color/system_accent2_200
- #410000
- @android:color/system_accent2_900
- @android:color/system_neutral1_100
- @android:color/system_accent3_200
- @android:color/system_accent3_900
- @android:color/system_accent1_200
- @android:color/system_accent2_100
- #FFDAD5
- @android:color/system_accent1_900
- @android:color/system_accent1_200
- @android:color/system_accent2_100
- @android:color/system_accent3_100
- @android:color/system_accent3_100
- @android:color/system_accent1_100
- @android:color/system_neutral1_50
- @android:color/system_accent1_100
- @android:color/system_accent2_0
- @android:color/system_accent3_0
- #FFFFFF
- @android:color/system_neutral2_700
- @android:color/system_neutral2_500
- @android:color/system_neutral2_200
- @android:color/system_accent1_0
- @android:color/system_neutral1_900
- @android:color/system_accent1_600
- @android:color/system_accent2_600
- @android:color/system_accent3_600
diff --git a/res/values-v34/colors.xml b/res/values-v34/colors.xml
index d19d4a6ffd..4f3a769a61 100644
--- a/res/values-v34/colors.xml
+++ b/res/values-v34/colors.xml
@@ -30,4 +30,105 @@
@android:color/system_on_surface_variant_light
+
+ @android:color/system_primary_container_light
+ @android:color/system_on_primary_container_light
+ @android:color/system_primary_light
+ @android:color/system_on_primary_light
+ @android:color/system_secondary_container_light
+ @android:color/system_on_secondary_container_light
+ @android:color/system_secondary_light
+ @android:color/system_on_secondary_light
+ @android:color/system_tertiary_container_light
+ @android:color/system_on_tertiary_container_light
+ @android:color/system_tertiary_light
+ @android:color/system_on_tertiary_light
+ @android:color/system_background_light
+ @android:color/system_on_background_light
+ @android:color/system_surface_light
+ @android:color/system_on_surface_light
+ @android:color/system_surface_container_low_light
+ @android:color/system_surface_container_lowest_light
+ @android:color/system_surface_container_light
+ @android:color/system_surface_container_high_light
+ @android:color/system_surface_container_highest_light
+ @android:color/system_surface_bright_light
+ @android:color/system_surface_dim_light
+ @android:color/system_surface_variant_light
+ @android:color/system_on_surface_variant_light
+ @android:color/system_outline_light
+ @android:color/system_outline_variant_light
+ @android:color/system_error_light
+ @android:color/system_on_error_light
+ @android:color/system_error_container_light
+ @android:color/system_on_error_container_light
+ @android:color/system_control_activated_light
+ @android:color/system_control_normal_light
+ @android:color/system_control_highlight_light
+ @android:color/system_text_primary_inverse_light
+ @android:color/system_text_secondary_and_tertiary_inverse_light
+ @android:color/system_text_primary_inverse_disable_only_light
+ @android:color/system_text_secondary_and_tertiary_inverse_disabled_light
+ @android:color/system_text_hint_inverse_light
+ @android:color/system_palette_key_color_primary_light
+ @android:color/system_palette_key_color_secondary_light
+ @android:color/system_palette_key_color_tertiary_light
+ @android:color/system_palette_key_color_neutral_light
+ @android:color/system_palette_key_color_neutral_variant_light
+ @android:color/system_primary_container_dark
+ @android:color/system_on_primary_container_dark
+ @android:color/system_primary_dark
+ @android:color/system_on_primary_dark
+ @android:color/system_secondary_container_dark
+ @android:color/system_on_secondary_container_dark
+ @android:color/system_secondary_dark
+ @android:color/system_on_secondary_dark
+ @android:color/system_tertiary_container_dark
+ @android:color/system_on_tertiary_container_dark
+ @android:color/system_tertiary_dark
+ @android:color/system_on_tertiary_dark
+ @android:color/system_background_dark
+ @android:color/system_on_background_dark
+ @android:color/system_surface_dark
+ @android:color/system_on_surface_dark
+ @android:color/system_surface_container_low_dark
+ @android:color/system_surface_container_lowest_dark
+ @android:color/system_surface_container_dark
+ @android:color/system_surface_container_high_dark
+ @android:color/system_surface_container_highest_dark
+ @android:color/system_surface_bright_dark
+ @android:color/system_surface_dim_dark
+ @android:color/system_surface_variant_dark
+ @android:color/system_on_surface_variant_dark
+ @android:color/system_outline_dark
+ @android:color/system_outline_variant_dark
+ @android:color/system_error_dark
+ @android:color/system_on_error_dark
+ @android:color/system_error_container_dark
+ @android:color/system_on_error_container_dark
+ @android:color/system_control_activated_dark
+ @android:color/system_control_normal_dark
+ @android:color/system_control_highlight_dark
+ @android:color/system_text_primary_inverse_dark
+ @android:color/system_text_secondary_and_tertiary_inverse_dark
+ @android:color/system_text_primary_inverse_disable_only_dark
+ @android:color/system_text_secondary_and_tertiary_inverse_disabled_dark
+ @android:color/system_text_hint_inverse_dark
+ @android:color/system_palette_key_color_primary_dark
+ @android:color/system_palette_key_color_secondary_dark
+ @android:color/system_palette_key_color_tertiary_dark
+ @android:color/system_palette_key_color_neutral_dark
+ @android:color/system_palette_key_color_neutral_variant_dark
+ @android:color/system_primary_fixed
+ @android:color/system_primary_fixed_dim
+ @android:color/system_on_primary_fixed
+ @android:color/system_on_primary_fixed_variant
+ @android:color/system_secondary_fixed
+ @android:color/system_secondary_fixed_dim
+ @android:color/system_on_secondary_fixed
+ @android:color/system_on_secondary_fixed_variant
+ @android:color/system_tertiary_fixed
+ @android:color/system_tertiary_fixed_dim
+ @android:color/system_on_tertiary_fixed
+ @android:color/system_on_tertiary_fixed_variant
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index eda36474b6..6151b5fb93 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -45,6 +45,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -582,51 +631,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 02f69f6e48..c82358b5da 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,8 +17,7 @@
** limitations under the License.
*/
-->
-
+
#FFC1C1C1
@@ -105,7 +104,7 @@
#FAF9F8
#1F1F1F
#4C4D50
- @color/material_color_on_surface_variant
+ @color/system_on_surface_variant_light
#1F1F1F
#444746
#C2E7FF
@@ -119,14 +118,14 @@
#C4C7C5
#0B57D0
#0B57D0
- @color/material_color_on_surface
- @color/material_color_on_surface_variant
+ @color/system_on_surface_light
+ @color/system_on_surface_variant_light
#1F2020
#393939
#E3E3E3
#CCCDCF
- @color/material_color_on_surface_variant
+ @color/system_on_surface_variant_dark
#E3E3E3
#C4C7C5
#004A77
@@ -140,51 +139,107 @@
#444746
#062E6F
#FFFFFF
- @color/material_color_on_surface
- @color/material_color_on_surface_variant
+ @color/system_on_surface_dark
+ @color/system_on_surface_variant_dark
- #3F4759
- #583E5B
- #FFFFFF
- #2B4678
- #141B2C
- #29132D
- #F5F3F7
- #001A41
- #BFC6DC
- #410000
- #141B2C
- #E3E2E6
- #DEBCDF
- #29132D
- #ADC6FF
- #DBE2F9
- #FFDAD5
- #001A41
- #ADC6FF
- #DBE2F9
- #121316
- #E1E2EC
- #FBD7FC
- #FBD7FC
- #D8E2FF
- #1B1B1F
- #D8E2FF
- #FFFFFF
- #FFFFFF
- #DBD9DD
- #FAF9FD
- #FFFFFF
- #FAF9FD
- #E9E7EC
- #E3E2E6
- #44474F
- #72747D
- #C4C7C5
- #FFFFFF
- #1B1B1F
- #EFEDF1
- #445E91
- #575E71
- #715573
+ #D9E2FF
+ #001945
+ #475D92
+ #FFFFFF
+ #DCE2F9
+ #151B2C
+ #575E71
+ #FFFFFF
+ #FDD7FA
+ #2A122C
+ #725572
+ #FFFFFF
+ #FAF8FF
+ #1A1B20
+ #FAF8FF
+ #1A1B20
+ #F4F3FA
+ #FFFFFF
+ #EEEDF4
+ #E8E7EF
+ #E2E2E9
+ #FAF8FF
+ #DAD9E0
+ #E1E2EC
+ #44464F
+ #757780
+ #C5C6D0
+ #BA1A1A
+ #FFFFFF
+ #FFDAD6
+ #410002
+ #D9E2FF
+ #44464F
+ #000000
+ #E2E2E9
+ #C5C6D0
+ #E2E2E9
+ #E2E2E9
+ #E2E2E9
+ #6076AC
+ #70778B
+ #8C6D8C
+ #76777D
+ #757780
+ #2F4578
+ #D9E2FF
+ #B0C6FF
+ #152E60
+ #404659
+ #DCE2F9
+ #C0C6DC
+ #2A3042
+ #593D59
+ #FDD7FA
+ #E0BBDD
+ #412742
+ #121318
+ #E2E2E9
+ #121318
+ #E2E2E9
+ #1A1B20
+ #0C0E13
+ #1E1F25
+ #282A2F
+ #33343A
+ #38393F
+ #121318
+ #44464F
+ #C5C6D0
+ #8F9099
+ #44464F
+ #FFB4AB
+ #690005
+ #93000A
+ #FFDAD6
+ #2F4578
+ #C5C6D0
+ #FFFFFF
+ #1A1B20
+ #44464F
+ #1A1B20
+ #1A1B20
+ #1A1B20
+ #6076AC
+ #70778B
+ #8C6D8C
+ #76777D
+ #757780
+ #D9E2FF
+ #B0C6FF
+ #001945
+ #2F4578
+ #DCE2F9
+ #C0C6DC
+ #151B2C
+ #404659
+ #FDD7FA
+ #E0BBDD
+ #2A122C
+ #593D59
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 7b43a3bd2a..ee7ed2687f 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,7 +17,7 @@
*/
-->
-
+
-
+
+
+
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
deleted file mode 100644
index 79b81871cc..0000000000
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2016 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.accessibility;
-
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.OnHierarchyChangeListener;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragOptions;
-
-import java.util.function.Function;
-
-/**
- * Utility listener to enable/disable accessibility drag flags for a ViewGroup
- * containing CellLayouts
- */
-public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener {
-
- private final ViewGroup mViewGroup;
- private final Function mDelegateFactory;
-
- /**
- * @param parent the viewgroup containing all the children
- * @param delegateFactory function to create no delegates
- */
- public AccessibleDragListenerAdapter(ViewGroup parent,
- Function delegateFactory) {
- mViewGroup = parent;
- mDelegateFactory = delegateFactory;
- }
-
- @Override
- public void onDragStart(DragObject dragObject, DragOptions options) {
- mViewGroup.setOnHierarchyChangeListener(this);
- enableAccessibleDrag(true, dragObject);
- }
-
- @Override
- public void onDragEnd() {
- mViewGroup.setOnHierarchyChangeListener(null);
- enableAccessibleDrag(false, null);
- Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
- }
-
-
- @Override
- public void onChildViewAdded(View parent, View child) {
- if (parent == mViewGroup) {
- setEnableForLayout((CellLayout) child, true);
- }
- }
-
- @Override
- public void onChildViewRemoved(View parent, View child) {
- if (parent == mViewGroup) {
- setEnableForLayout((CellLayout) child, false);
- }
- }
-
- protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
- for (int i = 0; i < mViewGroup.getChildCount(); i++) {
- setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
- }
- }
-
- protected final void setEnableForLayout(CellLayout layout, boolean enable) {
- layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null);
- }
-}
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
new file mode 100644
index 0000000000..21c2cafa3c
--- /dev/null
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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.accessibility
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.views.ActivityContext
+import java.util.function.Function
+
+/**
+ * Utility listener to enable/disable accessibility drag flags for a ViewGroup containing
+ * CellLayouts
+ */
+open class AccessibleDragListenerAdapter
+/**
+ * @param parent the viewgroup containing all the children
+ * @param delegateFactory function to create no delegates
+ */
+(
+ private val mViewGroup: ViewGroup,
+ private val mDelegateFactory: Function
+) : DragController.DragListener, ViewGroup.OnHierarchyChangeListener {
+ override fun onDragStart(dragObject: DragObject, options: DragOptions) {
+ mViewGroup.setOnHierarchyChangeListener(this)
+ enableAccessibleDrag(true, dragObject)
+ }
+
+ override fun onDragEnd() {
+ mViewGroup.setOnHierarchyChangeListener(null)
+ enableAccessibleDrag(false, null)
+ val activityContext = ActivityContext.lookupContext(mViewGroup.context) as ActivityContext
+ activityContext.getDragController>()?.removeDragListener(this)
+ }
+
+ override fun onChildViewAdded(parent: View, child: View) {
+ if (parent === mViewGroup) {
+ setEnableForLayout(child as CellLayout, true)
+ }
+ }
+
+ override fun onChildViewRemoved(parent: View, child: View) {
+ if (parent === mViewGroup) {
+ setEnableForLayout(child as CellLayout, false)
+ }
+ }
+
+ protected open fun enableAccessibleDrag(enable: Boolean, dragObject: DragObject?) {
+ for (i in 0 until mViewGroup.childCount) {
+ setEnableForLayout(mViewGroup.getChildAt(i) as CellLayout, enable)
+ }
+ }
+
+ protected fun setEnableForLayout(layout: CellLayout, enable: Boolean) {
+ layout.setDragAndDropAccessibilityDelegate(
+ if (enable) mDelegateFactory.apply(layout) else null
+ )
+ }
+}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index aefb7e92d5..c1264d6135 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -364,6 +364,7 @@ public class PrivateProfileManager extends UserProfileManager {
/** Add Private Space Header view elements based upon {@link UserProfileState} */
public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) {
mPSHeader = parent;
+ Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Binding private space.");
updateView();
if (mOnPSHeaderAdded != null) {
MAIN_EXECUTOR.execute(mOnPSHeaderAdded);
@@ -376,6 +377,8 @@ public class PrivateProfileManager extends UserProfileManager {
if (mPSHeader == null) {
return;
}
+ Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
+ + getCurrentState());
mPSHeader.setAlpha(1);
ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
assert lockPill != null;
@@ -761,6 +764,9 @@ public class PrivateProfileManager extends UserProfileManager {
alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ if (mFloatingMaskView == null) {
+ return;
+ }
mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue());
}
});
@@ -844,8 +850,22 @@ public class PrivateProfileManager extends UserProfileManager {
if (!Flags.privateSpaceAddFloatingMaskView()) {
return;
}
+ // Use getLocationOnScreen() as simply checking for mPSHeader.getBottom() is only relative
+ // to its parent.
+ int[] psHeaderLocation = new int[2];
+ mPSHeader.getLocationOnScreen(psHeaderLocation);
+ int psHeaderBottomY = psHeaderLocation[1] + mPsHeaderHeight;
+ // Calculate the topY of the floatingMaskView as if it was added.
+ int floatingMaskViewBottomBoxTopY =
+ (int) (mAllApps.getBottom() - getMainRecyclerView().getPaddingBottom());
+ // Don't attach if the header will be clipped by the floating mask view.
+ if (psHeaderBottomY > floatingMaskViewBottomBoxTopY) {
+ mFloatingMaskView = null;
+ return;
+ }
mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
R.layout.private_space_mask_view, mAllApps, false);
+ assert mFloatingMaskView != null;
mAllApps.addView(mFloatingMaskView);
// Translate off the screen first if its collapsing so this header view isn't visible to
// user when animation starts.
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
index ac9b146f12..eaeb8bbdd3 100644
--- a/src/com/android/launcher3/allapps/SectionDecorationHandler.java
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -25,7 +25,6 @@ import android.graphics.drawable.InsetDrawable;
import android.view.View;
import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
import com.android.launcher3.R;
import com.android.launcher3.util.Themes;
@@ -61,10 +60,10 @@ public class SectionDecorationHandler {
mContext = context;
mFillAlpha = fillAlpha;
- mFocusColor = ContextCompat.getColor(context,
- R.color.material_color_surface_bright); // UX recommended
- mFillColor = ContextCompat.getColor(context,
- R.color.material_color_surface_container_high); // UX recommended
+ mFocusColor = Themes.getAttrColor(context,
+ R.attr.materialColorSurfaceBright); // UX recommended
+ mFillColor = Themes.getAttrColor(context,
+ R.attr.materialColorSurfaceContainerHigh); // UX recommended
mIsTopLeftRound = isTopLeftRound;
mIsTopRightRound = isTopRightRound;
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index 3669d8b614..a7ab7b92aa 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -201,6 +201,7 @@ public class FolderGridOrganizer {
int row = rank / mCountX;
return col < PREVIEW_MAX_COLUMNS && row < PREVIEW_MAX_ROWS;
}
+ // If we have less than 4 items do this
return rank < MAX_NUM_ITEMS_IN_PREVIEW;
}
}
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index e6ade610f7..5faa2b83c3 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -52,6 +52,7 @@ import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.LooperIdleLock;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.util.ArrayList;
@@ -195,8 +196,8 @@ public class BaseLauncherBinder {
if (!WIDGETS_ENABLED) {
return;
}
- final List widgets =
- mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
+ List widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
+ .build(mBgDataModel.widgetsModel.getWidgetsByPackageItem());
executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
}
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 266ed0c67a..cf2cadc094 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -24,6 +24,7 @@ import com.android.launcher3.model.BgDataModel.FixedContainerItems
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder
import java.util.Objects
import java.util.concurrent.Executor
import java.util.function.Predicate
@@ -78,7 +79,9 @@ class ModelTaskController(
}
fun bindUpdatedWidgets(dataModel: BgDataModel) {
- val widgets = dataModel.widgetsModel.getWidgetsListForPicker(app.context)
+ val widgets =
+ WidgetsListBaseEntriesBuilder(app.context)
+ .build(dataModel.widgetsModel.widgetsByPackageItem)
scheduleCallbackTask { it.bindAllWidgets(widgets) }
}
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index 58ebf0fa13..35064cf094 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -26,7 +26,6 @@ import com.android.launcher3.AppFilter;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabelAndIcon;
import com.android.launcher3.icons.IconCache;
@@ -39,9 +38,6 @@ import com.android.launcher3.util.Preconditions;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import com.android.launcher3.widget.WidgetSections;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.wm.shell.Flags;
import java.util.ArrayList;
@@ -75,6 +71,9 @@ public class WidgetsModel {
* Returns all widgets keyed by their component key.
*/
public synchronized Map getWidgetsByComponentKey() {
+ if (!WIDGETS_ENABLED) {
+ return Collections.emptyMap();
+ }
return mWidgetsByPackageItem.values().stream()
.flatMap(Collection::stream).distinct()
.collect(Collectors.toMap(
@@ -87,51 +86,10 @@ public class WidgetsModel {
* Returns widgets grouped by the package item that they should belong to.
*/
public synchronized Map> getWidgetsByPackageItem() {
- return mWidgetsByPackageItem;
- }
-
- /**
- * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
- * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
- * list of {@link WidgetsListBaseEntry}s is not sorted.
- *
- * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
- */
- public synchronized ArrayList getFilteredWidgetsListForPicker(
- Context context,
- Predicate widgetItemFilter) {
if (!WIDGETS_ENABLED) {
- return new ArrayList<>();
+ return Collections.emptyMap();
}
- ArrayList result = new ArrayList<>();
- AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
-
- for (Map.Entry> entry :
- mWidgetsByPackageItem.entrySet()) {
- PackageItemInfo pkgItem = entry.getKey();
- List widgetItems = entry.getValue()
- .stream()
- .filter(widgetItemFilter).toList();
- if (!widgetItems.isEmpty()) {
- String sectionName = (pkgItem.title == null) ? "" :
- indexer.computeSectionName(pkgItem.title);
- result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
- result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
- }
- }
- return result;
- }
-
- /**
- * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
- * are sorted (based on label and user), but the overall list of
- * {@link WidgetsListBaseEntry}s is not sorted.
- *
- * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
- */
- public synchronized ArrayList getWidgetsListForPicker(Context context) {
- // return all items
- return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true);
+ return mWidgetsByPackageItem;
}
/**
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 6d6b3b6497..f895b302c5 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -17,6 +17,9 @@
package com.android.launcher3.recyclerview
import android.content.Context
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -38,21 +41,18 @@ const val EXTRA_ICONS_COUNT = 2
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
-class AllAppsRecyclerViewPool : RecycledViewPool() {
+class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : ActivityContext {
var hasWorkProfile = false
- private var mCancellableTask: CancellableTask>? = null
+ @VisibleForTesting(otherwise = PROTECTED)
+ var mCancellableTask: CancellableTask>? = null
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
- fun preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+ fun preInflateAllAppsViewHolders(context: T) {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
- val preInflateCount = getPreinflateCount(context)
- if (preInflateCount <= 0) {
- return
- }
// Create a separate context dedicated for all apps preinflation thread. The goal is to
// create a separate AssetManager obj internally to avoid lock contention with
@@ -77,34 +77,51 @@ class AllAppsRecyclerViewPool : RecycledViewPool() {
null
) {
override fun setAppsPerRow(appsPerRow: Int) = Unit
+
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
+ preInflateAllAppsViewHolders(adapter, BaseAllAppsAdapter.VIEW_TYPE_ICON, activeRv) {
+ getPreinflateCount(context)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PROTECTED)
+ fun preInflateAllAppsViewHolders(
+ adapter: RecyclerView.Adapter<*>,
+ viewType: Int,
+ parent: ViewGroup,
+ preInflationCountProvider: () -> Int
+ ) {
+ val preinflationCount = preInflationCountProvider.invoke()
+ if (preinflationCount <= 0) {
+ return
+ }
mCancellableTask?.cancel()
var task: CancellableTask>? = null
task =
CancellableTask(
{
val list: ArrayList = ArrayList()
- for (i in 0 until preInflateCount) {
+ for (i in 0 until preinflationCount) {
if (task?.canceled == true) {
break
}
- list.add(
- adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
- )
+ list.add(adapter.createViewHolder(parent, viewType))
}
list
},
MAIN_EXECUTOR,
{ viewHolders ->
- for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+ // Run preInflationCountProvider again as the needed VH might have changed
+ val newPreinflationCount = preInflationCountProvider.invoke()
+ for (i in 0 until minOf(viewHolders.size, newPreinflationCount)) {
putRecycledView(viewHolders[i])
}
}
)
mCancellableTask = task
- VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
+ VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
}
/**
@@ -125,7 +142,7 @@ class AllAppsRecyclerViewPool : RecycledViewPool() {
* app icons in size of one all apps pages, so that opening all apps don't need to inflate app
* icons.
*/
- fun getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+ fun getPreinflateCount(context: T): Int {
var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index 12eff612be..8ee799a2dd 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -24,7 +24,10 @@ import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.content.Context;
import android.content.Intent;
+import androidx.annotation.VisibleForTesting;
+
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
/**
* Utility class for tracking if the screen is currently on or off
@@ -34,8 +37,7 @@ public class ScreenOnTracker implements SafeCloseable {
public static final MainThreadInitializedObject INSTANCE =
new MainThreadInitializedObject<>(ScreenOnTracker::new);
- private final SimpleBroadcastReceiver mReceiver =
- new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+ private final SimpleBroadcastReceiver mReceiver;
private final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>();
private final Context mContext;
@@ -44,8 +46,20 @@ public class ScreenOnTracker implements SafeCloseable {
private ScreenOnTracker(Context context) {
// Assume that the screen is on to begin with
mContext = context;
+ mReceiver = new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+ init();
+ }
+
+ @VisibleForTesting
+ ScreenOnTracker(Context context, SimpleBroadcastReceiver receiver) {
+ mContext = context;
+ mReceiver = receiver;
+ init();
+ }
+
+ private void init() {
mIsScreenOn = true;
- mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+ mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
}
@Override
@@ -53,7 +67,8 @@ public class ScreenOnTracker implements SafeCloseable {
mReceiver.unregisterReceiverSafely(mContext);
}
- private void onReceive(Intent intent) {
+ @VisibleForTesting
+ void onReceive(Intent intent) {
String action = intent.getAction();
if (ACTION_SCREEN_ON.equals(action)) {
mIsScreenOn = true;
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index e413d7ff9c..2fa8bf4681 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -24,6 +24,7 @@ import android.view.ViewGroup;
import androidx.annotation.AnyThread;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.util.ViewPool.Reusable;
@@ -43,9 +44,16 @@ public class ViewPool {
public ViewPool(Context context, @Nullable ViewGroup parent,
int layoutId, int maxSize, int initialSize) {
+ this(LayoutInflater.from(context).cloneInContext(context),
+ parent, layoutId, maxSize, initialSize);
+ }
+
+ @VisibleForTesting
+ ViewPool(LayoutInflater inflater, @Nullable ViewGroup parent,
+ int layoutId, int maxSize, int initialSize) {
mLayoutId = layoutId;
mParent = parent;
- mInflater = LayoutInflater.from(context);
+ mInflater = inflater;
mPool = new Object[maxSize];
if (initialSize > 0) {
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 104209ef53..856f4b36b6 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -110,7 +110,7 @@ public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHo
}
View background = RoundedCornerEnforcement.findBackground(this);
if (background == null
- || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) {
resetRoundedCorners();
return;
}
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index 1e46ffd30c..2e5e251f01 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -67,7 +67,7 @@ public class RoundedCornerEnforcement {
/**
* Check whether the app widget has opted out of the enforcement.
*/
- public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+ public static boolean hasAppWidgetOptedOut(@NonNull View background) {
return background.getId() == android.R.id.background && background.getClipToOutline();
}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
new file mode 100644
index 0000000000..1abe4da98b
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.widget.model
+
+import android.content.Context
+import com.android.launcher3.compat.AlphabeticIndexCompat
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import java.util.function.Predicate
+
+/**
+ * A helper class that builds the list of [WidgetsListBaseEntry]s used by the UI to display widgets.
+ */
+class WidgetsListBaseEntriesBuilder(val context: Context) {
+
+ /** Builds the widgets list entries in a format understandable by the widget picking UI. */
+ @JvmOverloads
+ fun build(
+ widgetsByPackageItem: Map>,
+ widgetFilter: Predicate = Predicate { true },
+ ): List {
+ val indexer = AlphabeticIndexCompat(context)
+
+ return buildList {
+ for ((pkgItem, widgetItems) in widgetsByPackageItem.entries) {
+ val filteredWidgetItems = widgetItems.filter { widgetFilter.test(it) }
+ if (filteredWidgetItems.isNotEmpty()) {
+ // Enables fast scroll popup to show right characters in all locales.
+ val sectionName = pkgItem.title?.let { indexer.computeSectionName(it) } ?: ""
+
+ add(WidgetsListHeaderEntry.create(pkgItem, sectionName, filteredWidgetItems))
+ add(WidgetsListContentEntry(pkgItem, sectionName, filteredWidgetItems))
+ }
+ }
+ }
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
new file mode 100644
index 0000000000..e03ee46811
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.accessibility
+
+import android.content.Context
+import android.view.ViewGroup
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.util.ActivityContextWrapper
+import java.util.function.Function
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AccessibleDragListenerAdapterTest {
+
+ private lateinit var mockViewGroup: ViewGroup
+ private lateinit var mockChildOne: CellLayout
+ private lateinit var mockChildTwo: CellLayout
+ private lateinit var mDelegateFactory: Function
+ private lateinit var adapter: AccessibleDragListenerAdapter
+ private lateinit var mContext: Context
+
+ @Before
+ fun setup() {
+ mContext = ActivityContextWrapper(getApplicationContext())
+ mockViewGroup = mock(ViewGroup::class.java)
+ mockChildOne = mock(CellLayout::class.java)
+ mockChildTwo = mock(CellLayout::class.java)
+ `when`(mockViewGroup.context).thenReturn(mContext)
+ `when`(mockViewGroup.childCount).thenReturn(2)
+ `when`(mockViewGroup.getChildAt(0)).thenReturn(mockChildOne)
+ `when`(mockViewGroup.getChildAt(1)).thenReturn(mockChildTwo)
+ // Mock Delegate factory
+ mDelegateFactory =
+ mock(Function::class.java) as Function
+ `when`(mDelegateFactory.apply(any()))
+ .thenReturn(mock(DragAndDropAccessibilityDelegate::class.java))
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ }
+
+ @Test
+ fun `onDragStart enables accessible drag for all view children`() {
+ // Create mock view children
+ val mockDragObject = mock(DragObject::class.java)
+ val mockDragOptions = mock(DragOptions::class.java)
+
+ // Action
+ adapter.onDragStart(mockDragObject, mockDragOptions)
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+ verify(mockChildTwo).setDragAndDropAccessibilityDelegate(any())
+ }
+
+ @Test
+ fun `onDragEnd removes the accessibility delegate`() {
+ // Action
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ adapter.onDragEnd()
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+ verify(mockChildTwo).setDragAndDropAccessibilityDelegate(null)
+ }
+
+ @Test
+ fun `onChildViewAdded sets enabled as true for childview`() {
+ // Action
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ adapter.onChildViewAdded(mockViewGroup, mockChildOne)
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+ }
+
+ @Test
+ fun `onChildViewRemoved sets enabled as false for childview`() {
+ // Action
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ adapter.onChildViewRemoved(mockViewGroup, mockChildOne)
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
new file mode 100644
index 0000000000..3dd8dbceec
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -0,0 +1,266 @@
+/*
+ * 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.pm
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionHelperTest {
+
+ private val launcherModelHelper = LauncherModelHelper()
+ private val sandboxContext = spy(launcherModelHelper.sandboxContext)
+ private val packageManager = sandboxContext.packageManager
+ private val expectedAppPackage = "expectedAppPackage"
+ private val expectedInstallerPackage = "expectedInstallerPackage"
+ private val mockPackageInstaller: PackageInstaller = mock()
+
+ private lateinit var installSessionHelper: InstallSessionHelper
+ private lateinit var launcherApps: LauncherApps
+
+ @Before
+ fun setup() {
+ whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
+ whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
+ launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+ installSessionHelper = InstallSessionHelper(sandboxContext)
+ }
+
+ @Test
+ fun `getActiveSessions fetches verified install sessions from LauncherApps`() {
+ // Given
+ val expectedVerifiedSession1 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 0
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ val expectedVerifiedSession2 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+ // When
+ val actualSessions = installSessionHelper.getActiveSessions()
+ // Then
+ assertThat(actualSessions.values.toList()).isEqualTo(expectedSessions)
+ }
+
+ @Test
+ fun `getActiveSessionInfo fetches verified install sessions for given user and pkg`() {
+ // Given
+ val expectedVerifiedSession =
+ PackageInstaller.SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions)
+ .thenReturn(listOf(expectedVerifiedSession))
+ // When
+ val actualSession =
+ installSessionHelper.getActiveSessionInfo(UserHandle(0), expectedAppPackage)
+ // Then
+ assertThat(actualSession).isEqualTo(expectedVerifiedSession)
+ }
+
+ @Test
+ fun `getVerifiedSessionInfo verifies and returns session for given id`() {
+ // Given
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ whenever(mockPackageInstaller.getSessionInfo(1)).thenReturn(expectedSession)
+ // When
+ val actualSession = installSessionHelper.getVerifiedSessionInfo(1)
+ // Then
+ assertThat(actualSession).isEqualTo(expectedSession)
+ }
+
+ @Test
+ fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
+ // Given
+ val expectedApplicationInfo =
+ ApplicationInfo().apply {
+ flags = flags or FLAG_INSTALLED
+ enabled = true
+ }
+ doReturn(expectedApplicationInfo)
+ .whenever(launcherApps)
+ .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+ // When
+ val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+
+ @Test
+ fun `getAllVerifiedSessions verifies and returns all active install sessions`() {
+ // Given
+ val expectedVerifiedSession1 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 0
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ val expectedVerifiedSession2 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+ // When
+ val actualSessions = installSessionHelper.allVerifiedSessions
+ // Then
+ assertThat(actualSessions).isEqualTo(expectedSessions)
+ }
+
+ @Test
+ fun `promiseIconAddedForId returns true if there is a promiseIcon with the session id`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = false
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ actualResult = installSessionHelper.promiseIconAddedForId(1)
+ }
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+
+ @Test
+ fun `removePromiseIconId removes promiseIconId for given Session id`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = true
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ installSessionHelper.removePromiseIconId(1)
+ actualResult = installSessionHelper.promiseIconAddedForId(1)
+ }
+ // Then
+ assertThat(actualResult).isFalse()
+ }
+
+ @Test
+ fun `tryQueuePromiseAppIcon will update promise icon ids from eligible sessions`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "appPackage"
+ userId = 0
+ appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+ appLabel = "appLabel"
+ installReason = PackageManager.INSTALL_REASON_USER
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var wasPromiseIconAdded = false
+ var actualPromiseIconIds = ""
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ installSessionHelper.removePromiseIconId(1)
+ installSessionHelper.tryQueuePromiseAppIcon(expectedSession)
+ wasPromiseIconAdded = installSessionHelper.promiseIconAddedForId(1)
+ actualPromiseIconIds = LauncherPrefs.get(sandboxContext).get(PROMISE_ICON_IDS)
+ }
+ // Then
+ assertThat(wasPromiseIconAdded).isTrue()
+ assertThat(actualPromiseIconIds).isEqualTo(expectedIdString)
+ }
+
+ @Test
+ fun `verifySessionInfo is true if can verify given SessionInfo`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "appPackage"
+ userId = 0
+ appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+ appLabel = "appLabel"
+ installReason = PackageManager.INSTALL_REASON_USER
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = false
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ actualResult = installSessionHelper.verifySessionInfo(expectedSession)
+ }
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
new file mode 100644
index 0000000000..82043130a9
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.recyclerview
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.launcher3.views.ActivityContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsRecyclerViewPoolTest where T : Context, T : ActivityContext {
+
+ private lateinit var underTest: AllAppsRecyclerViewPool
+ private lateinit var adapter: RecyclerView.Adapter<*>
+
+ @Mock private lateinit var parent: ViewGroup
+ @Mock private lateinit var itemView: View
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = spy(AllAppsRecyclerViewPool())
+ adapter =
+ object : RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ object : ViewHolder(itemView) {}
+
+ override fun getItemCount() = 0
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+ }
+ underTest.setMaxRecycledViews(VIEW_TYPE, 20)
+ }
+
+ @Test
+ fun preinflate_success() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
+ }
+
+ @Test
+ fun preinflate_not_triggered() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 0 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_before_runOnMainThread() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+
+ underTest.clear()
+
+ awaitTasksCompleted()
+ verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_after_run() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+ awaitTasksCompleted()
+
+ underTest.clear()
+
+ verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.VIEW_PREINFLATION_EXECUTOR.submit { null }.get()
+ Executors.MAIN_EXECUTOR.submit { null }.get()
+ }
+
+ companion object {
+ private const val VIEW_TYPE: Int = 4
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
index 191d284939..4217d22790 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.util;
+import android.R;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.ContextThemeWrapper;
@@ -39,11 +40,16 @@ public class ActivityContextWrapper extends ContextThemeWrapper implements Activ
private final MyDragLayer mMyDragLayer;
public ActivityContextWrapper(Context base) {
- super(base, android.R.style.Theme_DeviceDefault);
+ this(base, R.style.Theme_DeviceDefault);
+ }
+
+ public ActivityContextWrapper(Context base, int theme) {
+ super(base, theme);
mProfile = InvariantDeviceProfile.INSTANCE.get(base).getDeviceProfile(base).copy(base);
mMyDragLayer = new MyDragLayer(this);
}
+
@Override
public BaseDragLayer getDragLayer() {
return mMyDragLayer;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
index 093afc9060..94bd7c96ba 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
@@ -16,17 +16,20 @@
package com.android.launcher3.util
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoMoreInteractions
@SmallTest
+@RunWith(AndroidJUnit4::class)
class RunnableListTest {
@Mock private lateinit var runnable1: Runnable
@@ -73,7 +76,7 @@ class RunnableListTest {
underTest.executeAllAndDestroy()
- verifyZeroInteractions(runnable1)
+ verifyNoMoreInteractions(runnable1)
}
@Test
@@ -107,7 +110,7 @@ class RunnableListTest {
underTest.remove(runnable1)
underTest.executeAllAndClear()
- verifyZeroInteractions(runnable1)
+ verifyNoMoreInteractions(runnable1)
verify(runnable2).run()
}
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
new file mode 100644
index 0000000000..430aad25e7
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_SCREEN_OFF
+import android.content.Intent.ACTION_SCREEN_ON
+import android.content.Intent.ACTION_USER_PRESENT
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ScreenOnTrackerTest {
+
+ @Mock private lateinit var receiver: SimpleBroadcastReceiver
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var listener: ScreenOnTracker.ScreenOnListener
+
+ private lateinit var underTest: ScreenOnTracker
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ underTest = ScreenOnTracker(context, receiver)
+ }
+
+ @Test
+ fun test_default_state() {
+ verify(receiver).register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT)
+ assertThat(underTest.isScreenOn).isTrue()
+ }
+
+ @Test
+ fun close_unregister_receiver() {
+ underTest.close()
+
+ verify(receiver).unregisterReceiverSafely(context)
+ }
+
+ @Test
+ fun add_listener_then_receive_screen_on_intent_notify_listener() {
+ underTest.addListener(listener)
+
+ underTest.onReceive(Intent(ACTION_SCREEN_ON))
+
+ verify(listener).onScreenOnChanged(true)
+ assertThat(underTest.isScreenOn).isTrue()
+ }
+
+ @Test
+ fun add_listener_then_receive_screen_off_intent_notify_listener() {
+ underTest.addListener(listener)
+
+ underTest.onReceive(Intent(ACTION_SCREEN_OFF))
+
+ verify(listener).onScreenOnChanged(false)
+ assertThat(underTest.isScreenOn).isFalse()
+ }
+
+ @Test
+ fun add_listener_then_receive_user_present_intent_notify_listener() {
+ underTest.addListener(listener)
+
+ underTest.onReceive(Intent(ACTION_USER_PRESENT))
+
+ verify(listener).onUserPresent()
+ assertThat(underTest.isScreenOn).isTrue()
+ }
+
+ @Test
+ fun remove_listener_then_receive_intent_noOp() {
+ underTest.addListener(listener)
+
+ underTest.removeListener(listener)
+
+ underTest.onReceive(Intent(ACTION_USER_PRESENT))
+ verifyNoMoreInteractions(listener)
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
similarity index 100%
rename from tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
new file mode 100644
index 0000000000..612fcd428f
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.util
+
+import android.view.View
+import android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.launcher3.util.SystemUiController.UI_STATE_BASE_WINDOW
+import com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SystemUiControllerTest {
+
+ @Mock private lateinit var window: Window
+ @Mock private lateinit var decorView: View
+
+ private lateinit var underTest: SystemUiController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(window.decorView).thenReturn(decorView)
+ underTest = SystemUiController(window)
+ }
+
+ @Test
+ fun test_default_state() {
+ assertThat(underTest.toString()).isEqualTo("mStates=[0, 0, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_base_window_light() {
+ underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+ val visibility =
+ View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ verify(decorView).systemUiVisibility = eq(visibility)
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+ val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_scrim_view_light() {
+ underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ true)
+
+ verify(decorView).systemUiVisibility =
+ eq(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+ val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_base_window_dark() {
+ underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ false)
+
+ verify(decorView, never()).systemUiVisibility = anyInt()
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+ val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_scrim_view_dark() {
+ underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ false)
+
+ verify(decorView, never()).systemUiVisibility = anyInt()
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+ val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+ }
+
+ @Test
+ fun get_base_sysui_visibility() {
+ `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+ }
+
+ @Test
+ fun update_state_base_window_light_with_existing_visibility() {
+ `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+ underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+ val visibility =
+ View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
+ SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+ verify(decorView).systemUiVisibility = eq(visibility)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
index bad21c925d..d82818d6d6 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
@@ -18,6 +18,7 @@ package com.android.launcher3.util
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.R
import com.android.launcher3.util.ViewCache.CacheEntry
@@ -26,6 +27,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@SmallTest
@RunWith(AndroidJUnit4::class)
class ViewCacheTest {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
new file mode 100644
index 0000000000..e535656815
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.same
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@UiThreadTest
+class ViewPoolTest {
+
+ @Mock private lateinit var viewParent: ViewGroup
+ @Mock private lateinit var view: ReusableView
+ @Mock private lateinit var inflater: LayoutInflater
+
+ private lateinit var underTest: ViewPool
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(inflater.cloneInContext(any())).thenReturn(inflater)
+ underTest = ViewPool(inflater, viewParent, LAYOUT_ID, 10, 0)
+ }
+
+ @Test
+ fun get_view_from_empty_pool_inflate_new_view() {
+ underTest.view
+
+ verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+ }
+
+ @Test
+ fun recycle_view() {
+ underTest.recycle(view)
+
+ val returnedView = underTest.view
+
+ verify(view).onRecycle()
+ assertThat(returnedView).isSameInstanceAs(view)
+ verifyNoMoreInteractions(inflater)
+ }
+
+ @Test
+ fun get_view_twice_from_view_pool_with_one_view() {
+ underTest.recycle(view)
+ underTest.view
+ verifyNoMoreInteractions(inflater)
+
+ underTest.view
+
+ verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+ }
+
+ companion object {
+ private const val LAYOUT_ID = 1000
+ }
+
+ private inner class ReusableView(context: Context) : View(context), ViewPool.Reusable {
+ override fun onRecycle() {
+ TODO("Not yet implemented")
+ }
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
new file mode 100644
index 0000000000..db7770205e
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.widget
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RoundedCornerEnforcementTest {
+
+ @Test
+ fun `Widget view has one background`() {
+ val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+
+ doReturn(android.R.id.background).whenever(mockWidgetView).id
+
+ assertSame(RoundedCornerEnforcement.findBackground(mockWidgetView), mockWidgetView)
+ }
+
+ @Test
+ fun `Widget opted out of rounded corner enforcement`() {
+ val mockView = mock(View::class.java)
+
+ doReturn(android.R.id.background).whenever(mockView).id
+ doReturn(true).whenever(mockView).clipToOutline
+
+ assertTrue(RoundedCornerEnforcement.hasAppWidgetOptedOut(mockView))
+ }
+
+ @Test
+ fun `Compute rect based on widget view with background`() {
+ val mockBackgroundView = mock(View::class.java)
+ val mockWidgetView = mock(ViewGroup::class.java)
+ val testRect = Rect(0, 0, 0, 0)
+
+ doReturn(WIDTH).whenever(mockBackgroundView).width
+ doReturn(HEIGHT).whenever(mockBackgroundView).height
+ doReturn(LEFT).whenever(mockBackgroundView).left
+ doReturn(TOP).whenever(mockBackgroundView).top
+ doReturn(mockWidgetView).whenever(mockBackgroundView).parent
+
+ RoundedCornerEnforcement.computeRoundedRectangle(
+ mockWidgetView,
+ mockBackgroundView,
+ testRect
+ )
+
+ assertEquals(Rect(50, 75, 250, 275), testRect)
+ }
+
+ @Test
+ fun `Compute system radius`() {
+ val mockContext = mock(Context::class.java)
+ val mockRes = mock(Resources::class.java)
+
+ doReturn(mockRes).whenever(mockContext).resources
+ doReturn(RADIUS)
+ .whenever(mockRes)
+ .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+ doReturn(LAUNCHER_RADIUS)
+ .whenever(mockRes)
+ .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+ assertEquals(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+ }
+
+ companion object {
+ const val WIDTH = 200
+ const val HEIGHT = 200
+ const val LEFT = 50
+ const val TOP = 75
+
+ const val RADIUS = 8f
+ const val LAUNCHER_RADIUS = 16f
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
new file mode 100644
index 0000000000..5df7caa372
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.widget.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetsListBaseEntriesBuilderTest {
+ @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+ @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var iconCache: IconCache
+
+ private lateinit var userHandle: UserHandle
+ private lateinit var context: Context
+ private lateinit var testInvariantProfile: InvariantDeviceProfile
+ private lateinit var allWidgets: Map>
+ private lateinit var underTest: WidgetsListBaseEntriesBuilder
+
+ @Before
+ fun setUp() {
+ userHandle = UserHandle.CURRENT
+ context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ testInvariantProfile = LauncherAppState.getIDP(context)
+
+ doAnswer { invocation: InvocationOnMock ->
+ val componentWithLabel = invocation.getArgument(0) as ComponentWithLabel
+ componentWithLabel.getComponent().shortClassName
+ }
+ .`when`(iconCache)
+ .getTitleNoCache(any())
+ underTest = WidgetsListBaseEntriesBuilder(context)
+
+ allWidgets =
+ mapOf(
+ // app 1
+ packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE) to
+ listOf(
+ createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME),
+ createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+ ),
+ // app 2
+ packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE) to
+ listOf(createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)),
+ // app 3
+ packageItemInfoWithTitle(APP_3_PACKAGE_NAME, APP_3_PACKAGE_TITLE) to
+ listOf(createWidgetItem(APP_3_PACKAGE_NAME, APP_3_PROVIDER_1_CLASS_NAME))
+ )
+ }
+
+ @Test
+ fun widgetsListBaseEntriesBuilder_addsHeaderAndContentEntries_withCorrectSectionName() {
+ val expectedWidgetsCountBySection =
+ listOf(
+ APP_1_EXPECTED_SECTION_NAME to 2,
+ APP_2_EXPECTED_SECTION_NAME to 1,
+ APP_3_EXPECTED_SECTION_NAME to 1
+ )
+
+ val entries = underTest.build(allWidgets)
+
+ assertThat(entries).hasSize(6)
+ val headerEntrySectionAndWidgetSizes =
+ entries.filterIsInstance().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ val contentEntrySectionAndWidgetSizes =
+ entries.filterIsInstance().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ assertThat(headerEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ assertThat(contentEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ }
+
+ @Test
+ fun widgetsListBaseEntriesBuilder_withFilter_addsFilteredHeaderAndContentEntries() {
+ val allowList = listOf(APP_1_PROVIDER_1_CLASS_NAME, APP_3_PROVIDER_1_CLASS_NAME)
+ val expectedWidgetsCountBySection =
+ listOf(
+ APP_1_EXPECTED_SECTION_NAME to 1, // one widget filtered out
+ APP_3_EXPECTED_SECTION_NAME to 1
+ )
+
+ val entries =
+ underTest.build(allWidgets) { w -> allowList.contains(w.componentName.shortClassName) }
+
+ assertThat(entries).hasSize(4) // app 2 filtered out
+ val headerEntrySectionAndWidgetSizes =
+ entries.filterIsInstance().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ val contentEntrySectionAndWidgetSizes =
+ entries.filterIsInstance().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ assertThat(headerEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ assertThat(contentEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ }
+
+ private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+ val packageItemInfo = PackageItemInfo(packageName, userHandle)
+ packageItemInfo.title = title
+ return packageItemInfo
+ }
+
+ private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+ val providerInfo =
+ WidgetUtils.createAppWidgetProviderInfo(
+ ComponentName.createRelative(packageName, widgetProviderName)
+ )
+ val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+ return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+ }
+
+ companion object {
+ const val APP_1_PACKAGE_NAME = "com.example.app1"
+ const val APP_1_PACKAGE_TITLE = "App1"
+ const val APP_1_EXPECTED_SECTION_NAME = "A" // for fast popup
+ const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+ const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+ const val APP_2_PACKAGE_NAME = "com.example.app2"
+ const val APP_2_PACKAGE_TITLE = "SomeApp2"
+ const val APP_2_EXPECTED_SECTION_NAME = "S" // for fast popup
+ const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+ const val APP_3_PACKAGE_NAME = "com.example.app3"
+ const val APP_3_PACKAGE_TITLE = "OtherApp3"
+ const val APP_3_EXPECTED_SECTION_NAME = "O" // for fast popup
+ const val APP_3_PROVIDER_1_CLASS_NAME = "app3Provider1"
+ }
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
index eac7f63fb9..de48432492 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
@@ -116,7 +116,8 @@ public class PrivateSpaceHeaderViewTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = new ActivityContextWrapper(getApplicationContext());
+ mContext = new ActivityContextWrapper(getApplicationContext(),
+ R.style.DynamicColorsBaseLauncherTheme);
when(mAllApps.getContext()).thenReturn(mContext);
when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO);
when(mUserCache.getUserProfiles())
diff --git a/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt b/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
new file mode 100644
index 0000000000..e8681dc4a5
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.folder
+
+import android.graphics.Point
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class TestCase(val maxCountX: Int, val maxCountY: Int, val totalItems: Int)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderPagedViewTest {
+
+ companion object {
+ private fun makeFolderGridOrganizer(testCase: TestCase): FolderGridOrganizer {
+ val folderGridOrganizer = FolderGridOrganizer(testCase.maxCountX, testCase.maxCountY)
+ folderGridOrganizer.setContentSize(testCase.totalItems)
+ return folderGridOrganizer
+ }
+ }
+
+ @Test
+ fun setContentSize() {
+ assertCountXandY(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedCountX = 4,
+ expectedCountY = 3
+ )
+ assertCountXandY(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 8),
+ expectedCountX = 3,
+ expectedCountY = 3
+ )
+ assertCountXandY(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 3),
+ expectedCountX = 2,
+ expectedCountY = 2
+ )
+ }
+
+ private fun assertCountXandY(testCase: TestCase, expectedCountX: Int, expectedCountY: Int) {
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ assert(folderGridOrganizer.countX == expectedCountX) {
+ "Error on expected countX $expectedCountX got ${folderGridOrganizer.countX} using test case $testCase"
+ }
+ assert(folderGridOrganizer.countY == expectedCountY) {
+ "Error on expected countY $expectedCountY got ${folderGridOrganizer.countY} using test case $testCase"
+ }
+ }
+
+ @Test
+ fun getPosForRank() {
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(0, 0),
+ rank = 0
+ )
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(1, 0),
+ rank = 1
+ )
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(3, 0),
+ rank = 3
+ )
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(2, 1),
+ rank = 6
+ )
+ val testCase = TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22)
+ // Rank 16 and 38 should yield the same point since 38 % 12 == 2
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ assertFolderRank(testCase, expectedPos = folderGridOrganizer.getPosForRank(2), rank = 38)
+ }
+
+ private fun assertFolderRank(testCase: TestCase, expectedPos: Point, rank: Int) {
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ val pos = folderGridOrganizer.getPosForRank(rank)
+ assert(pos == expectedPos) {
+ "Expected pos = $expectedPos doesn't match pos = $pos for the given rank $rank and the give test case $testCase"
+ }
+ }
+
+ @Test
+ fun isItemInPreview() {
+ val folderGridOrganizer = FolderGridOrganizer(5, 8)
+ folderGridOrganizer.setContentSize(ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1)
+ // Very few items
+ for (i in 0..3) {
+ assertItemsInPreview(
+ TestCase(
+ maxCountX = 5,
+ maxCountY = 8,
+ totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+ ),
+ expectedIsInPreview = true,
+ page = 0,
+ rank = i
+ )
+ }
+ for (i in 4..40) {
+ assertItemsInPreview(
+ TestCase(
+ maxCountX = 5,
+ maxCountY = 8,
+ totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+ ),
+ expectedIsInPreview = false,
+ page = 0,
+ rank = i
+ )
+ }
+ // Full of items
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = false,
+ page = 0,
+ rank = 2
+ )
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = false,
+ page = 0,
+ rank = 2
+ )
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = true,
+ page = 0,
+ rank = 5
+ )
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = true,
+ page = 0,
+ rank = 6
+ )
+ }
+
+ private fun assertItemsInPreview(
+ testCase: TestCase,
+ expectedIsInPreview: Boolean,
+ page: Int,
+ rank: Int
+ ) {
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ val isInPreview = folderGridOrganizer.isItemInPreview(page, rank)
+ assert(isInPreview == expectedIsInPreview) {
+ "Item preview state should be $expectedIsInPreview but got $isInPreview, for page $page and rank $rank, for test case $testCase"
+ }
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 25eae442a9..ad37f7b801 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -98,6 +98,7 @@ import java.util.concurrent.TimeoutException;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -2398,6 +2399,16 @@ public final class LauncherInstrumentation {
disableSensorRotation();
final Integer initialPid = getPid();
final LogEventChecker eventChecker = new LogEventChecker(this);
+ eventChecker.setLogExclusionRule(event -> {
+ Matcher matcher = Pattern.compile("KeyEvent.*flags=0x([0-9a-fA-F]+)").matcher(event);
+ if (matcher.find()) {
+ int keyEventFlags = Integer.parseInt(matcher.group(1), 16);
+ // ignore KeyEvents with FLAG_CANCELED
+ return (keyEventFlags & KeyEvent.FLAG_CANCELED) != 0;
+ }
+ return false;
+ });
+
if (eventChecker.start()) mEventChecker = eventChecker;
return () -> {
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 055a357049..3a49160088 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -35,6 +35,8 @@ public class LogEventChecker {
// Map from an event sequence name to an ordered list of expected events in that sequence.
private final ListMap mExpectedEvents = new ListMap<>();
+ private LogExclusionRule mLogExclusionRule = null;
+
LogEventChecker(LauncherInstrumentation launcher) {
mLauncher = launcher;
}
@@ -48,6 +50,10 @@ public class LogEventChecker {
mExpectedEvents.add(sequence, pattern);
}
+ void setLogExclusionRule(LogExclusionRule logExclusionRule) {
+ mLogExclusionRule = logExclusionRule;
+ }
+
// Waits for the expected number of events and returns them.
private ListMap finishSync(long waitForExpectedCountMs) {
final long startTime = SystemClock.uptimeMillis();
@@ -74,7 +80,9 @@ public class LogEventChecker {
final ListMap eventSequences = new ListMap<>();
for (String rawEvent : rawEvents) {
final String[] split = rawEvent.split("/");
- eventSequences.add(split[0], split[1]);
+ if (mLogExclusionRule == null || !mLogExclusionRule.shouldExclude(split[1])) {
+ eventSequences.add(split[0], split[1]);
+ }
}
return eventSequences;
}
@@ -175,4 +183,8 @@ public class LogEventChecker {
return list;
}
}
+
+ interface LogExclusionRule {
+ boolean shouldExclude(String event);
+ }
}