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); + } }