diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 5e325fb97f..cf9a68b095 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -529,52 +529,26 @@ public class TaskbarActivityContext extends BaseTaskbarContext { /** * Creates {@link WindowManager.LayoutParams} for Taskbar, and also sets LP.paramsForRotation - * for taskbar showing as navigation bar + * for taskbar */ private WindowManager.LayoutParams createAllWindowParams() { final int windowType = ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL; WindowManager.LayoutParams windowLayoutParams = createDefaultWindowLayoutParams(windowType, TaskbarActivityContext.WINDOW_TITLE); - if (!isPhoneButtonNavMode()) { - return windowLayoutParams; - } - // Provide WM layout params for all rotations to cache, see NavigationBar#getBarLayoutParams - int width = WindowManager.LayoutParams.MATCH_PARENT; - int height = WindowManager.LayoutParams.MATCH_PARENT; - int gravity = Gravity.BOTTOM; windowLayoutParams.paramsForRotation = new WindowManager.LayoutParams[4]; for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { WindowManager.LayoutParams lp = createDefaultWindowLayoutParams(windowType, TaskbarActivityContext.WINDOW_TITLE); - switch (rot) { - case Surface.ROTATION_0, Surface.ROTATION_180 -> { - // Defaults are fine - width = WindowManager.LayoutParams.MATCH_PARENT; - height = mLastRequestedNonFullscreenSize; - gravity = Gravity.BOTTOM; - } - case Surface.ROTATION_90 -> { - width = mLastRequestedNonFullscreenSize; - height = WindowManager.LayoutParams.MATCH_PARENT; - gravity = Gravity.END; - } - case Surface.ROTATION_270 -> { - width = mLastRequestedNonFullscreenSize; - height = WindowManager.LayoutParams.MATCH_PARENT; - gravity = Gravity.START; - } - + if (isPhoneButtonNavMode()) { + populatePhoneButtonNavModeWindowLayoutParams(rot, lp); } - lp.width = width; - lp.height = height; - lp.gravity = gravity; windowLayoutParams.paramsForRotation[rot] = lp; } - // Override current layout params + // Override with current layout params WindowManager.LayoutParams currentParams = windowLayoutParams.paramsForRotation[getDisplay().getRotation()]; windowLayoutParams.width = currentParams.width; @@ -584,6 +558,32 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return windowLayoutParams; } + /** + * Update {@link WindowManager.LayoutParams} with values specific to phone and 3 button + * navigation users + */ + private void populatePhoneButtonNavModeWindowLayoutParams(int rot, + WindowManager.LayoutParams lp) { + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + lp.gravity = Gravity.BOTTOM; + + // Override with per-rotation specific values + switch (rot) { + case Surface.ROTATION_0, Surface.ROTATION_180 -> { + lp.height = mLastRequestedNonFullscreenSize; + } + case Surface.ROTATION_90 -> { + lp.width = mLastRequestedNonFullscreenSize; + lp.gravity = Gravity.END; + } + case Surface.ROTATION_270 -> { + lp.width = mLastRequestedNonFullscreenSize; + lp.gravity = Gravity.START; + } + } + } + public void onConfigurationChanged(@Config int configChanges) { mControllers.onConfigurationChanged(configChanges); if (!mIsUserSetupComplete) { @@ -944,8 +944,14 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } if (landscapePhoneButtonNav) { mWindowLayoutParams.width = size; + for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { + mWindowLayoutParams.paramsForRotation[rot].width = size; + } } else { mWindowLayoutParams.height = size; + for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { + mWindowLayoutParams.paramsForRotation[rot].height = size; + } } mControllers.taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); notifyUpdateLayoutParams(); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index aa457ca4f1..567fad02ac 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -118,11 +118,9 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas getProvidedInsets(insetsRoundedCornerFlag) } - if (!context.isGestureNav) { - if (windowLayoutParams.paramsForRotation != null) { - for (layoutParams in windowLayoutParams.paramsForRotation) { - layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag) - } + if (windowLayoutParams.paramsForRotation != null) { + for (layoutParams in windowLayoutParams.paramsForRotation) { + layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag) } } @@ -156,19 +154,12 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas ) } - val gravity = windowLayoutParams.gravity - // Pre-calculate insets for different providers across different rotations for this gravity for (rotation in Surface.ROTATION_0..Surface.ROTATION_270) { // Add insets for navbar rotated params - if (windowLayoutParams.paramsForRotation != null) { - val layoutParams = windowLayoutParams.paramsForRotation[rotation] - for (provider in layoutParams.providedInsets) { - setProviderInsets(provider, layoutParams.gravity, rotation) - } - } - for (provider in windowLayoutParams.providedInsets) { - setProviderInsets(provider, gravity, rotation) + val layoutParams = windowLayoutParams.paramsForRotation[rotation] + for (provider in layoutParams.providedInsets) { + setProviderInsets(provider, layoutParams.gravity, rotation) } } context.notifyUpdateLayoutParams() diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index 629c951932..3d584642b9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -900,12 +900,12 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } // Only update the following flags when system gesture is not in progress. + updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false); setStashedImeState(); } private void setStashedImeState() { boolean shouldStashForIme = shouldStashForIme(); - updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false); if (hasAnyFlag(FLAG_STASHED_IN_APP_IME) != shouldStashForIme) { updateStateForFlag(FLAG_STASHED_IN_APP_IME, shouldStashForIme); applyState(TASKBAR_STASH_DURATION_FOR_IME, getTaskbarStashStartDelayForIme()); diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java index dcc3b052c9..873dea80e3 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java +++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java @@ -21,6 +21,7 @@ import android.app.PendingIntent; import android.app.Person; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; @@ -157,6 +158,28 @@ public class ApiWrapper { } } + /** + * Returns an intent which can be used to open Private Space Settings. + */ + public static Intent getPrivateSpaceSettingsIntent(Context context) { + if (android.os.Flags.allowPrivateProfile() && Flags.enablePrivateSpace()) { + LauncherApps launcherApps = context.getSystemService(LauncherApps.class); + IntentSender intentSender = launcherApps.getPrivateSpaceSettingsIntent(); + if (intentSender == null) { + return null; + } + StartActivityParams params = new StartActivityParams((PendingIntent) null, 0); + params.intentSender = intentSender; + ActivityOptions options = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode(ActivityOptions + .MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + params.options = options.toBundle(); + params.requireActivityResult = false; + return ProxyActivityStarter.getLaunchIntent(context, params); + } + return null; + } + /** * Checks if an activity is flagged as non-resizeable. */ diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java index 8648b56072..f345aebb0c 100644 --- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java @@ -304,6 +304,12 @@ public class LandscapePagedViewHandler implements RecentsPagedOrientationHandler } } + @Override + public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, + float taskMenuX, float taskMenuY) { + return (int) (taskMenuX - taskInsetMargin); + } + @Override public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, LinearLayout taskMenuLayout, int dividerSpacing, diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java index 60e6a255cb..5cd97763d1 100644 --- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java @@ -209,6 +209,12 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements : thumbnailView.getMeasuredWidth()) - (2 * padding); } + @Override + public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, + float taskMenuX, float taskMenuY) { + return (int) (deviceProfile.availableHeightPx - taskInsetMargin - taskMenuY); + } + @Override public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, LinearLayout taskMenuLayout, int dividerSpacing, diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java index 01c1225c40..4b65d53172 100644 --- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java @@ -176,6 +176,9 @@ public interface RecentsPagedOrientationHandler extends PagedOrientationHandler View taskMenuView, float taskInsetMargin, View taskViewIcon); int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, @StagePosition int stagePosition); + + int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, float taskMenuX, + float taskMenuY); /** * Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items * inside task menu view. diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java index a964639e41..89c678c114 100644 --- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java @@ -113,6 +113,12 @@ public class SeascapePagedViewHandler extends LandscapePagedViewHandler { } } + @Override + public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, + float taskMenuX, float taskMenuY) { + return (int) (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX); + } + @Override public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo, int desiredStagePosition) { diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java index 137455e23a..c9aad1a4d6 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java @@ -140,11 +140,9 @@ public class TaskMenuView extends AbstractFloatingView { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (!enableOverviewIconMenu()) { - int maxMenuHeight = calculateMaxHeight(); - if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) { - heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST); - } + int maxMenuHeight = calculateMaxHeight(); + if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @@ -416,10 +414,9 @@ public class TaskMenuView extends AbstractFloatingView { * with a margin on the top and bottom. */ private int calculateMaxHeight() { - float taskBottom = mTaskView.getHeight() + mTaskView.getPersistentTranslationY(); float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); - - return (int) (taskBottom - taskInsetMargin - getTranslationY()); + return mTaskView.getPagedOrientationHandler().getTaskMenuHeight(taskInsetMargin, + mActivity.getDeviceProfile(), getTranslationX(), getTranslationY()); } private void setOnClosingStartCallback(Runnable onClosingStartCallback) { diff --git a/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java similarity index 100% rename from quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java similarity index 100% rename from quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java similarity index 100% rename from quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java diff --git a/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/NavigationBarRotationContextTest.java similarity index 100% rename from quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/NavigationBarRotationContextTest.java diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt similarity index 100% rename from quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskGridNavHelperTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java similarity index 100% rename from quickstep/tests/src/com/android/quickstep/util/TaskGridNavHelperTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java similarity index 100% rename from quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java similarity index 100% rename from quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index 45a95277b5..5bcf72a3b5 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -503,7 +503,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @Test @PortraitLandscape - @ScreenRecord // b/326839375 public void testOverviewDeadzones() throws Exception { startTestAppsWithCheck(); diff --git a/res/layout/widget_cell.xml b/res/layout/widget_cell.xml index 55dd1de034..4533873071 100644 --- a/res/layout/widget_cell.xml +++ b/res/layout/widget_cell.xml @@ -17,7 +17,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0dp" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/widget_cell_horizontal_padding" + android:layout_marginStart="@dimen/widget_cell_horizontal_padding" + android:layout_marginEnd="@dimen/widget_cell_horizontal_padding" android:paddingVertical="@dimen/widget_cell_vertical_padding" android:layout_weight="1" android:orientation="vertical" diff --git a/res/layout/widget_recommendations_table.xml b/res/layout/widget_recommendations_table.xml index e3f05620cd..b53d2d55b4 100644 --- a/res/layout/widget_recommendations_table.xml +++ b/res/layout/widget_recommendations_table.xml @@ -17,5 +17,4 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/widget_recommendations_table_horizontal_padding" android:paddingVertical="@dimen/widget_recommendations_table_vertical_padding" /> diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml index 3c79588258..27aba6bfe7 100644 --- a/res/values-sw720dp/dimens.xml +++ b/res/values-sw720dp/dimens.xml @@ -37,6 +37,7 @@ 30dp + 16dp 24dp diff --git a/res/values/dimens.xml b/res/values/dimens.xml index a912e2d0e8..97737fbd71 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -176,7 +176,7 @@ 8dp - 16dp + 8dp 14sp 24dp 8dp @@ -187,7 +187,6 @@ 117dp 0dp 8dp - 16dp 16dp @@ -198,7 +197,8 @@ 20dp 2dp - 16dp + + 11dp 0dp 24dp diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 42d4d50218..2e0f6762b4 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -72,6 +72,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class InvariantDeviceProfile { @@ -577,6 +578,45 @@ public class InvariantDeviceProfile { return filteredProfiles; } + /** + * Returns the GridOption associated to the given file name or null if the fileName is not + * supported. + * Ej, launcher.db -> "normal grid", launcher_4_by_4.db -> "practical grid" + */ + public GridOption getGridOptionFromFileName(Context context, String fileName) { + return parseAllGridOptions(context).stream() + .filter(gridOption -> Objects.equals(gridOption.dbFile, fileName)) + .findFirst() + .orElse(null); + } + + /** + * Returns the name of the given size on the current device or empty string if the size is not + * supported. Ej. 4x4 -> normal, 5x4 -> practical, etc. + * (Note: the name of the grid can be different for the same grid size depending of + * the values of the InvariantDeviceProfile) + * + */ + public String getGridNameFromSize(Context context, Point size) { + return parseAllGridOptions(context).stream() + .filter(gridOption -> gridOption.numColumns == size.x + && gridOption.numRows == size.y) + .map(gridOption -> gridOption.name) + .findFirst() + .orElse(""); + } + + /** + * Returns the grid option for the given gridName on the current device (Note: the gridOption + * be different for the same gridName depending on the values of the InvariantDeviceProfile). + */ + public GridOption getGridOptionFromName(Context context, String gridName) { + return parseAllGridOptions(context).stream() + .filter(gridOption -> Objects.equals(gridOption.name, gridName)) + .findFirst() + .orElse(null); + } + /** * @return all the grid options that can be shown on the device */ diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 8277c3e038..72977ee7a9 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -2365,7 +2365,8 @@ public class Launcher extends StatefulActivity * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close * animation. * - * @param preferredItemId The id of the preferred item to match to if it exists. + * @param preferredItemId The id of the preferred item to match to if it exists, + * or ItemInfo#NO_MATCHING_ID if you want to not match by item id * @param packageName The package name of the app to match. * @param user The user of the app to match. * @param supportsAllAppsState If true and we are in All Apps state, looks for view in All Apps. diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 34ebaf2539..84b8ba1126 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -138,6 +138,11 @@ public class LauncherSettings { */ public static final int ITEM_TYPE_SEARCH_ACTION = 9; + /** + * Private space install app button. + */ + public static final int ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON = 11; + /** * The custom icon bitmap. *

Type: BLOB

@@ -206,6 +211,8 @@ public class LauncherSettings { case ITEM_TYPE_TASK: return "TASK"; case ITEM_TYPE_QSB: return "QSB"; case ITEM_TYPE_APP_PAIR: return "APP_PAIR"; + case ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON: + return "PRIVATE_SPACE_INSTALL_APP_BUTTON"; default: return String.valueOf(type); } } diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java index 1ebd49e9dd..e7bb1d0089 100644 --- a/src/com/android/launcher3/allapps/PrivateProfileManager.java +++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java @@ -23,15 +23,12 @@ import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.os.UserHandle; import android.os.UserManager; @@ -43,6 +40,7 @@ import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.PrivateSpaceInstallAppButtonInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.Preconditions; @@ -60,10 +58,6 @@ import java.util.function.Predicate; */ public class PrivateProfileManager extends UserProfileManager { - // TODO (b/324573634): Fix the intent string. - public static final Intent PRIVATE_SPACE_INTENT = new - Intent("com.android.settings.action.PRIVATE_SPACE_SETUP_FLOW"); - private final ActivityAllAppsContainerView mAllApps; private final Predicate mPrivateProfileMatcher; private Set mPreInstalledSystemPackages = new HashSet<>(); @@ -105,13 +99,13 @@ public class PrivateProfileManager extends UserProfileManager { context, com.android.launcher3.R.drawable.private_space_install_app_icon); BitmapInfo bitmapInfo = LauncherIcons.obtain(context).createIconBitmap(shortcut); - AppInfo itemInfo = new AppInfo(); + PrivateSpaceInstallAppButtonInfo itemInfo = new PrivateSpaceInstallAppButtonInfo(); itemInfo.title = context.getResources().getString(R.string.ps_add_button_label); itemInfo.intent = mAppInstallerIntent; itemInfo.bitmap = bitmapInfo; itemInfo.contentDescription = context.getResources().getString( com.android.launcher3.R.string.ps_add_button_content_description); - itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP | FLAG_NOT_PINNABLE; + itemInfo.runtimeStatusFlags |= FLAG_NOT_PINNABLE; BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON); item.itemInfo = itemInfo; @@ -162,7 +156,8 @@ public class PrivateProfileManager extends UserProfileManager { /** Opens the Private Space Settings Page. */ public void openPrivateSpaceSettings() { if (mPrivateSpaceSettingsAvailable) { - mAllApps.getContext().startActivity(PRIVATE_SPACE_INTENT); + mAllApps.getContext() + .startActivity(ApiWrapper.getPrivateSpaceSettingsIntent(mAllApps.getContext())); } } @@ -194,9 +189,8 @@ public class PrivateProfileManager extends UserProfileManager { private void initializePrivateSpaceSettingsState() { Preconditions.assertNonUiThread(); - ResolveInfo resolveInfo = mAllApps.getContext().getPackageManager() - .resolveActivity(PRIVATE_SPACE_INTENT, PackageManager.MATCH_SYSTEM_ONLY); - setPrivateSpaceSettingsAvailable(resolveInfo != null); + Intent psSettingsIntent = ApiWrapper.getPrivateSpaceSettingsIntent(mAllApps.getContext()); + setPrivateSpaceSettingsAvailable(psSettingsIntent != null); } private void setPreInstalledSystemPackages() { diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 55849c2149..f7cff78ba1 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -94,6 +94,10 @@ public class ItemInfo { * {@link Favorites#ITEM_TYPE_APP_PAIR}, * {@link Favorites#ITEM_TYPE_APPWIDGET} or * {@link Favorites#ITEM_TYPE_CUSTOM_APPWIDGET}. + * {@link Favorites#ITEM_TYPE_TASK}. + * {@link Favorites#ITEM_TYPE_QSB}. + * {@link Favorites#ITEM_TYPE_SEARCH_ACTION}. + * {@link Favorites#ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON}. */ public int itemType; diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java index 70cad968b4..9fbc6bf109 100644 --- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java @@ -120,11 +120,6 @@ public abstract class ItemInfoWithIcon extends ItemInfo { */ public static final int FLAG_ARCHIVED = 1 << 14; - /** - * Flag indicating it's the Private Space Install App icon. - */ - public static final int FLAG_PRIVATE_SPACE_INSTALL_APP = 1 << 15; - /** * Status associated with the system state of the underlying item. This is calculated every * time a new info is created and not persisted on the disk. diff --git a/src/com/android/launcher3/model/data/PrivateSpaceInstallAppButtonInfo.java b/src/com/android/launcher3/model/data/PrivateSpaceInstallAppButtonInfo.java new file mode 100644 index 0000000000..1e7281dd1d --- /dev/null +++ b/src/com/android/launcher3/model/data/PrivateSpaceInstallAppButtonInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model.data; + +import com.android.launcher3.LauncherSettings; + +/** + * Represents the Private Space Install App button in AllAppsView. + */ +public class PrivateSpaceInstallAppButtonInfo extends AppInfo { + + public PrivateSpaceInstallAppButtonInfo() { + itemType = LauncherSettings.Favorites.ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON; + } +} diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index 22bc13bb25..f2b7d18fed 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -50,8 +50,10 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; +import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherFiles; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; @@ -121,7 +123,48 @@ public class RestoreDbTask { // executed again. LauncherPrefs.get(context).removeSync(RESTORE_DEVICE); - idp.reinitializeAfterRestore(context); + if (Flags.narrowGridRestore()) { + String oldPhoneFileName = idp.dbFile; + removeOldDBs(context, oldPhoneFileName); + trySettingPreviousGidAsCurrent(context, idp, oldPhoneFileName); + } else { + idp.reinitializeAfterRestore(context); + } + } + + /** + * Try setting the gird used in the previous phone to the new one. If the current device doesn't + * support the previous grid option it will not be set. + */ + private static void trySettingPreviousGidAsCurrent(Context context, InvariantDeviceProfile idp, + String oldPhoneDbFileName) { + InvariantDeviceProfile.GridOption gridOption = idp.getGridOptionFromFileName(context, + oldPhoneDbFileName); + if (gridOption != null) { + /* + * We do this because in some cases different devices have different names for grid + * options, in one device the grid option "normal" can be 4x4 while in other it + * could be "practical". Calling this changes the current device grid to the same + * we had in the other phone, in the case the current phone doesn't support the grid + * option we use the default and migrate the db to the default. Migration occurs on + * {@code GridSizeMigrationUtil#migrateGridIfNeeded} + */ + idp.setCurrentGrid(context, gridOption.name); + } + } + + /** + * Only keep the last database used on the previous device. + */ + private static void removeOldDBs(Context context, String oldPhoneDbFileName) { + // At this point idp.dbFile contains the name of the dbFile from the previous phone + LauncherFiles.GRID_DB_FILES.stream() + .filter(dbName -> !dbName.equals(oldPhoneDbFileName)) + .forEach(dbName -> { + if (context.getDatabasePath(dbName).delete()) { + FileLog.d(TAG, "Removed old grid db file: " + dbName); + } + }); } private static boolean performRestore(Context context, ModelDbController controller) { diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index 111931eeeb..911568c3b6 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -369,8 +369,8 @@ public class ItemClickHandler { intent = ApiWrapper.getAppMarketActivityIntent(launcher, itemInfoWithIcon.getTargetComponent().getPackageName(), Process.myUserHandle()); - } else if ((itemInfoWithIcon.runtimeStatusFlags - & ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP) != 0) { + } else if (itemInfoWithIcon.itemType + == LauncherSettings.Favorites.ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON) { intent = ApiWrapper.getAppMarketActivityIntent(launcher, BuildConfig.APPLICATION_ID, launcher.getAppsView().getPrivateProfileManager().getProfileUser()); diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java index c60e1a41cd..cab798215c 100644 --- a/src/com/android/launcher3/views/FloatingSurfaceView.java +++ b/src/com/android/launcher3/views/FloatingSurfaceView.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.views; +import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID; import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView; import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; @@ -159,7 +160,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements if (mContract == null) { return; } - View icon = mLauncher.getFirstMatchForAppClose(-1, + View icon = mLauncher.getFirstMatchForAppClose(NO_MATCHING_ID, mContract.componentName.getPackageName(), mContract.user, false /* supportsAllAppsState */); diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index f2f83c8e5d..aaefe60f78 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -57,6 +57,8 @@ import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.util.CancellableTask; import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize; +import com.android.launcher3.widget.util.WidgetSizes; import java.util.function.Consumer; @@ -80,7 +82,7 @@ public class WidgetCell extends LinearLayout { * The requested scale of the preview container. It can be lower than this as well. */ private float mPreviewContainerScale = 1f; - + private Size mPreviewContainerSize = new Size(0, 0); private FrameLayout mWidgetImageContainer; private WidgetImageView mWidgetImage; private ImageView mWidgetBadge; @@ -176,6 +178,8 @@ public class WidgetCell extends LinearLayout { mWidgetDims.setText(null); mWidgetDescription.setText(null); mWidgetDescription.setVisibility(GONE); + showDescription(true); + showDimensions(true); if (mActiveRequest != null) { mActiveRequest.cancel(); @@ -186,6 +190,7 @@ public class WidgetCell extends LinearLayout { mWidgetImageContainer.removeView(mAppWidgetHostViewPreview); } mAppWidgetHostViewPreview = null; + mPreviewContainerSize = new Size(0, 0); mAppWidgetHostViewScale = 1f; mPreviewContainerScale = 1f; mItem = null; @@ -201,30 +206,21 @@ public class WidgetCell extends LinearLayout { * Applies the item to this view */ public void applyFromCellItem(WidgetItem item) { - applyFromCellItem(item, 1f); - } - - /** - * Applies the item to this view - */ - public void applyFromCellItem(WidgetItem item, float previewScale) { - applyFromCellItem(item, previewScale, this::applyPreview, null); + applyFromCellItem(item, this::applyPreview, /*cachedPreview=*/null); } /** * Applies the item to this view * @param item item to apply - * @param previewScale factor to scale the preview * @param callback callback when preview is loaded in case the preview is being loaded or cached * @param cachedPreview previously cached preview bitmap is present */ - public void applyFromCellItem(WidgetItem item, float previewScale, - @NonNull Consumer callback, @Nullable Bitmap cachedPreview) { - mPreviewContainerScale = previewScale; - + public void applyFromCellItem(WidgetItem item, @NonNull Consumer callback, + @Nullable Bitmap cachedPreview) { Context context = getContext(); mItem = item; mWidgetSize = getWidgetItemSizePx(getContext(), mActivity.getDeviceProfile(), mItem); + initPreviewContainerSizeAndScale(); mWidgetName.setText(mItem.label); mWidgetName.setContentDescription( @@ -278,6 +274,17 @@ public class WidgetCell extends LinearLayout { } } + private void initPreviewContainerSizeAndScale() { + WidgetPreviewContainerSize previewSize = WidgetPreviewContainerSize.Companion.forItem(mItem, + mActivity.getDeviceProfile()); + mPreviewContainerSize = WidgetSizes.getWidgetSizePx(mActivity.getDeviceProfile(), + previewSize.spanX, previewSize.spanY); + + float scaleX = (float) mPreviewContainerSize.getWidth() / mWidgetSize.getWidth(); + float scaleY = (float) mPreviewContainerSize.getHeight() / mWidgetSize.getHeight(); + mPreviewContainerScale = Math.min(scaleX, scaleY); + } + private void setAppWidgetHostViewPreview( NavigableAppWidgetHostView appWidgetHostViewPreview, LauncherAppWidgetProviderInfo providerInfo, @@ -383,6 +390,16 @@ public class WidgetCell extends LinearLayout { mWidgetDescription.setVisibility(show ? VISIBLE : GONE); } + /** + * Shows or hides the dimensions displayed below each widget. + * + * @param show a flag that shows the dimensions of the widget if {@code true}, hides it if + * {@code false}. + */ + public void showDimensions(boolean show) { + mWidgetDims.setVisibility(show ? VISIBLE : GONE); + } + /** * Set whether the app icon, for the app that provides the widget, should be shown next to the * title text of the widget. @@ -448,17 +465,22 @@ public class WidgetCell extends LinearLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams(); - - mAppWidgetHostViewScale = mPreviewContainerScale; int maxWidth = MeasureSpec.getSize(widthMeasureSpec); - containerLp.width = Math.round(mWidgetSize.getWidth() * mAppWidgetHostViewScale); + + // mPreviewContainerScale ensures the needed scaling with respect to original widget size. + mAppWidgetHostViewScale = mPreviewContainerScale; + containerLp.width = mPreviewContainerSize.getWidth(); + containerLp.height = mPreviewContainerSize.getHeight(); + + // If we don't have enough available width, scale the preview container to fit. if (containerLp.width > maxWidth) { containerLp.width = maxWidth; - mAppWidgetHostViewScale = (float) containerLp.width / mWidgetSize.getWidth(); + mAppWidgetHostViewScale = (float) containerLp.width / mPreviewContainerSize.getWidth(); + containerLp.height = Math.round( + mPreviewContainerSize.getHeight() * mAppWidgetHostViewScale); } - containerLp.height = Math.round(mWidgetSize.getHeight() * mAppWidgetHostViewScale); - // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass + // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass super.onMeasure(widthMeasureSpec, heightMeasureSpec); } diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java index 11f4485e33..f0a23be431 100644 --- a/src/com/android/launcher3/widget/WidgetImageView.java +++ b/src/com/android/launcher3/widget/WidgetImageView.java @@ -82,15 +82,27 @@ public class WidgetImageView extends View { private void updateDstRectF() { float myWidth = getWidth(); float myHeight = getHeight(); - float bitmapWidth = mDrawable.getIntrinsicWidth(); + final float bitmapWidth = mDrawable.getIntrinsicWidth(); + final float bitmapHeight = mDrawable.getIntrinsicHeight(); + final float bitmapAspectRatio = bitmapWidth / bitmapHeight; + final float containerAspectRatio = myWidth / myHeight; - final float scale = bitmapWidth > myWidth ? myWidth / bitmapWidth : 1; - float scaledWidth = bitmapWidth * scale; - float scaledHeight = mDrawable.getIntrinsicHeight() * scale; + // Scale by width if image has larger aspect ratio than the container else by height; and + // avoid cropping the previews + final float scale = bitmapAspectRatio > containerAspectRatio ? myWidth / bitmapWidth + : myHeight / bitmapHeight; - mDstRectF.left = (myWidth - scaledWidth) / 2; - mDstRectF.right = (myWidth + scaledWidth) / 2; + final float scaledWidth = bitmapWidth * scale; + final float scaledHeight = bitmapHeight * scale; + // Avoid cropping by checking bounds after scaling. + if (scaledWidth > myWidth) { + mDstRectF.left = 0; + mDstRectF.right = scaledWidth; + } else { + mDstRectF.left = (myWidth - scaledWidth) / 2; + mDstRectF.right = (myWidth + scaledWidth) / 2; + } if (scaledHeight > myHeight) { mDstRectF.top = 0; mDstRectF.bottom = scaledHeight; diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index ceb0072310..ab1ad70c24 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -16,7 +16,6 @@ package com.android.launcher3.widget; -import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY; import android.content.Context; @@ -188,13 +187,9 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { mWidgetCellHorizontalPadding) .forEach(row -> { TableRow tableRow = new TableRow(getContext()); - if (enableCategorizedWidgetSuggestions()) { - // Vertically center align items, so that even if they don't fill bounds, - // they can look organized when placed together in a row. - tableRow.setGravity(Gravity.CENTER_VERTICAL); - } else { - tableRow.setGravity(Gravity.TOP); - } + // Vertically center align items, so that even if they don't fill bounds, + // they can look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); row.forEach(widgetItem -> { WidgetCell widget = addItemCell(tableRow); widget.applyFromCellItem(widgetItem); diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java index 68f18aebf4..0d775c3532 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java @@ -36,41 +36,49 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { (context, entry) -> entry.mWidgets.stream() .map(item -> item.label).sorted().collect(Collectors.joining(", ")); - private static final BiFunction SUBTITLE_DEFAULT = - (context, entry) -> { - List items = entry.mWidgets; - int wc = (int) items.stream().filter(item -> item.widgetInfo != null).count(); - int sc = Math.max(0, items.size() - wc); + @Nullable + private static String buildWidgetsCountString(Context context, int wc, int sc) { + Resources resources = context.getResources(); + if (wc == 0 && sc == 0) { + return null; + } - Resources resources = context.getResources(); - if (wc == 0 && sc == 0) { - return null; - } - - String subtitle; - if (wc > 0 && sc > 0) { - String widgetsCount = PluralMessageFormat.getIcuPluralString(context, - R.string.widgets_count, wc); - String shortcutsCount = PluralMessageFormat.getIcuPluralString(context, - R.string.shortcuts_count, sc); - subtitle = resources.getString(R.string.widgets_and_shortcuts_count, - widgetsCount, shortcutsCount); - } else if (wc > 0) { - subtitle = PluralMessageFormat.getIcuPluralString(context, - R.string.widgets_count, wc); - } else { - subtitle = PluralMessageFormat.getIcuPluralString(context, - R.string.shortcuts_count, sc); - } - return subtitle; - }; + String subtitle; + if (wc > 0 && sc > 0) { + String widgetsCount = PluralMessageFormat.getIcuPluralString(context, + R.string.widgets_count, wc); + String shortcutsCount = PluralMessageFormat.getIcuPluralString(context, + R.string.shortcuts_count, sc); + subtitle = resources.getString(R.string.widgets_and_shortcuts_count, + widgetsCount, shortcutsCount); + } else if (wc > 0) { + subtitle = PluralMessageFormat.getIcuPluralString(context, + R.string.widgets_count, wc); + } else { + subtitle = PluralMessageFormat.getIcuPluralString(context, + R.string.shortcuts_count, sc); + } + return subtitle; + } private final boolean mIsWidgetListShown; + /** Selected widgets displayed */ + private final int mVisibleWidgetsCount; private final boolean mIsSearchEntry; + private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, + List items, int visibleWidgetsCount, + boolean isSearchEntry, boolean isWidgetListShown) { + super(pkgItem, titleSectionName, items); + mVisibleWidgetsCount = visibleWidgetsCount; + mIsSearchEntry = isSearchEntry; + mIsWidgetListShown = isWidgetListShown; + } + private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, List items, boolean isSearchEntry, boolean isWidgetListShown) { super(pkgItem, titleSectionName, items); + mVisibleWidgetsCount = (int) items.stream().filter(w -> w.widgetInfo != null).count(); mIsSearchEntry = isSearchEntry; mIsWidgetListShown = isWidgetListShown; } @@ -91,8 +99,13 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { @Nullable public String getSubtitle(Context context) { - return mIsSearchEntry - ? SUBTITLE_SEARCH.apply(context, this) : SUBTITLE_DEFAULT.apply(context, this); + if (mIsSearchEntry) { + return SUBTITLE_SEARCH.apply(context, this); + } else { + int shortcutsCount = Math.max(0, + (int) mWidgets.stream().filter(WidgetItem::isShortcut).count()); + return buildWidgetsCountString(context, mVisibleWidgetsCount, shortcutsCount); + } } @Override @@ -102,6 +115,7 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem) && mTitleSectionName.equals(otherEntry.mTitleSectionName) && mIsWidgetListShown == otherEntry.mIsWidgetListShown + && mVisibleWidgetsCount == otherEntry.mVisibleWidgetsCount && mIsSearchEntry == otherEntry.mIsSearchEntry; } @@ -112,6 +126,7 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { mPkgItem, mTitleSectionName, mWidgets, + mVisibleWidgetsCount, mIsSearchEntry, /* isWidgetListShown= */ true); } @@ -122,7 +137,28 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { pkgItem, titleSectionName, items, - /* forSearch */ false, + /* isSearchEntry= */ false, + /* isWidgetListShown= */ false); + } + + /** + * Creates a widget list holder for an header ("app" / "suggestions") which has widgets or/and + * shortcuts. + * + * @param pkgItem package item info for the header section + * @param titleSectionName title string for the header + * @param items all items for the given header + * @param visibleWidgetsCount widgets count when only selected widgets are shown due to + * limited space. + */ + public static WidgetsListHeaderEntry create(PackageItemInfo pkgItem, String titleSectionName, + List items, int visibleWidgetsCount) { + return new WidgetsListHeaderEntry( + pkgItem, + titleSectionName, + items, + visibleWidgetsCount, + /* isSearchEntry= */ false, /* isWidgetListShown= */ false); } @@ -132,7 +168,7 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { pkgItem, titleSectionName, items, - /* forSearch */ true, + /* isSearchEntry */ true, /* isWidgetListShown= */ false); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java index 426a3aeb33..811759dd85 100644 --- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java @@ -93,18 +93,19 @@ public final class WidgetRecommendationsView extends PagedView recommendedWidgets, DeviceProfile deviceProfile, final @Px float availableHeight, final @Px int availableWidth, final @Px int cellPadding) { this.mAvailableHeight = availableHeight; - removeAllViews(); + clear(); - maybeDisplayInTable(recommendedWidgets, deviceProfile, availableWidth, cellPadding); + int displayedWidgets = maybeDisplayInTable(recommendedWidgets, deviceProfile, + availableWidth, cellPadding); updateTitleAndIndicator(); - return getChildCount() > 0; + return displayedWidgets; } /** @@ -118,9 +119,9 @@ public final class WidgetRecommendationsView extends PagedView> recommendations, DeviceProfile deviceProfile, final @Px float availableHeight, final @Px int availableWidth, @@ -128,19 +129,23 @@ public final class WidgetRecommendationsView extends PagedView> entry : new TreeMap<>(recommendations).entrySet()) { // If none of the recommendations for the category could fit in the mAvailableHeight, we // don't want to add that category; and we look for the next one. - if (maybeDisplayInTable(entry.getValue(), deviceProfile, availableWidth, cellPadding)) { + int displayedCount = maybeDisplayInTable(entry.getValue(), deviceProfile, + availableWidth, cellPadding); + if (displayedCount > 0) { mCategoryTitles.add( context.getResources().getString(entry.getKey().categoryTitleRes)); displayedCategories++; + totalDisplayedWidgets += displayedCount; } if (displayedCategories == MAX_CATEGORIES) { @@ -150,7 +155,12 @@ public final class WidgetRecommendationsView extends PagedView 0; + return totalDisplayedWidgets; + } + + private void clear() { + mCategoryTitles.clear(); + removeAllViews(); } /** Displays the page title and paging indicator if there are multiple pages. */ @@ -199,21 +209,8 @@ public final class WidgetRecommendationsView extends PagedViewReturns false if none of the recommendations could fit.

*/ - private boolean maybeDisplayInTable(List recommendedWidgets, + private int maybeDisplayInTable(List recommendedWidgets, DeviceProfile deviceProfile, final @Px int availableWidth, final @Px int cellPadding) { Context context = getContext(); LayoutInflater inflater = LayoutInflater.from(context); + // Since we are limited by space, we don't sort recommendations - to show most relevant + // (if possible). List> rows = groupWidgetItemsUsingRowPxWithoutReordering( recommendedWidgets, context, @@ -249,13 +248,13 @@ public final class WidgetRecommendationsView extends PagedView 0) { addView(recommendationsTable); } - return displayedAtLeastOne; + return displayedCount; } /** Returns location of a widget cell for displaying the "touch and hold" education tip. */ diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index c0f1070b3d..848f6fa7bb 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -107,7 +107,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet entry -> mCurrentUser.equals(entry.mPkgItem.user); private final Predicate mWorkWidgetsFilter; protected final boolean mHasWorkProfile; - protected boolean mHasRecommendedWidgets; + // Number of recommendations displayed + protected int mRecommendedWidgetsCount; protected final SparseArray mAdapters = new SparseArray(); @Nullable private ArrowTipView mLatestEducationalTip; private final OnLayoutChangeListener mLayoutChangeListenerToShowTips = @@ -581,7 +582,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet } if (enableCategorizedWidgetSuggestions()) { - mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations( + mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(), mDeviceProfile, /* availableHeight= */ getMaxAvailableHeightForRecommendations(), @@ -589,7 +590,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet /* cellPadding= */ mWidgetCellHorizontalPadding ); } else { - mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations( + mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( mActivityContext.getPopupDataProvider().getRecommendedWidgets(), mDeviceProfile, /* availableHeight= */ getMaxAvailableHeightForRecommendations(), @@ -597,7 +598,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet /* cellPadding= */ mWidgetCellHorizontalPadding ); } - mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE); + mWidgetRecommendationsContainer.setVisibility( + mRecommendedWidgetsCount > 0 ? VISIBLE : GONE); } @Px @@ -790,7 +792,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet } /** private the height, in pixel, + the vertical margins of a given view. */ - private static int measureHeightWithVerticalMargins(View view) { + protected static int measureHeightWithVerticalMargins(View view) { if (view.getVisibility() != VISIBLE) { return 0; } diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index ef3ccf0f5b..36f8bf90ca 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -15,8 +15,6 @@ */ package com.android.launcher3.widget.picker; -import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; - import android.content.Context; import android.graphics.Bitmap; import android.util.Log; @@ -121,7 +119,7 @@ public final class WidgetsListTableViewHolderBinder widget.setVisibility(View.VISIBLE); // When preview loads, notify adapter to rebind the item and possibly animate - widget.applyFromCellItem(widgetItem, 1f, + widget.applyFromCellItem(widgetItem, bitmap -> holder.onPreviewLoaded(Pair.create(widgetItem, bitmap)), holder.previewCache.get(widgetItem)); widget.requestLayout(); @@ -150,13 +148,9 @@ public final class WidgetsListTableViewHolderBinder tableRow = (TableRow) table.getChildAt(i); } else { tableRow = new TableRow(table.getContext()); - if (enableCategorizedWidgetSuggestions()) { - // Vertically center align items, so that even if they don't fill bounds, they - // can look organized when placed together in a row. - tableRow.setGravity(Gravity.CENTER_VERTICAL); - } else { - tableRow.setGravity(Gravity.TOP); - } + // Vertically center align items, so that even if they don't fill bounds, they + // can look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); table.addView(tableRow); } if (tableRow.getChildCount() > widgetItems.size()) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java index 12564f4932..76b8401b7f 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -17,11 +17,13 @@ package com.android.launcher3.widget.picker; import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; +import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx; +import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_SIZE_COMPARATOR; + +import static java.lang.Math.max; import android.content.Context; import android.util.AttributeSet; -import android.util.Log; -import android.util.Size; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -30,26 +32,23 @@ import android.widget.TableLayout; import android.widget.TableRow; import androidx.annotation.Nullable; +import androidx.annotation.Px; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.widget.WidgetCell; -import com.android.launcher3.widget.util.WidgetSizes; +import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize; import java.util.ArrayList; import java.util.List; /** A {@link TableLayout} for showing recommended widgets. */ public final class WidgetsRecommendationTableLayout extends TableLayout { - private static final String TAG = "WidgetsRecommendationTableLayout"; - private static final float DOWN_SCALE_RATIO = 0.9f; - private static final float MAX_DOWN_SCALE_RATIO = 0.5f; private final float mWidgetsRecommendationTableVerticalPadding; private final float mWidgetCellVerticalPadding; private final float mWidgetCellTextViewsHeight; - private float mRecommendationTableMaxHeight = Float.MAX_VALUE; @Nullable private OnLongClickListener mWidgetCellOnLongClickListener; @Nullable private OnClickListener mWidgetCellOnClickListener; @@ -82,47 +81,40 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { * desired {@code recommendationTableMaxHeight}. * *

If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a - * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only - * row still doesn't fit, we scale down the preview image. + * last row from the {@code recommendedWidgets} until it fits or only one row left. * *

Returns {@code false} if none of the widgets could fit

*/ - public boolean setRecommendedWidgets(List> recommendedWidgets, - DeviceProfile deviceProfile, - float recommendationTableMaxHeight) { - mRecommendationTableMaxHeight = recommendationTableMaxHeight; - RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f, - deviceProfile, - recommendedWidgets); - bindData(data); - return !data.mRecommendationTable.isEmpty(); + public int setRecommendedWidgets(List> recommendedWidgets, + DeviceProfile deviceProfile, float recommendationTableMaxHeight) { + List> rows = selectRowsThatFitInAvailableHeight(recommendedWidgets, + recommendationTableMaxHeight, deviceProfile); + bindData(rows); + return rows.stream().mapToInt(ArrayList::size).sum(); } - private void bindData(RecommendationTableData data) { - if (data.mRecommendationTable.isEmpty()) { + private void bindData(List> recommendationTable) { + if (recommendationTable.isEmpty()) { setVisibility(GONE); return; } removeAllViews(); - for (int i = 0; i < data.mRecommendationTable.size(); i++) { - List widgetItems = data.mRecommendationTable.get(i); + for (int i = 0; i < recommendationTable.size(); i++) { + List widgetItems = recommendationTable.get(i); TableRow tableRow = new TableRow(getContext()); - if (enableCategorizedWidgetSuggestions()) { - // Vertically center align items, so that even if they don't fill bounds, they can - // look organized when placed together in a row. - tableRow.setGravity(Gravity.CENTER_VERTICAL); - } else { - tableRow.setGravity(Gravity.TOP); - } + // Vertically center align items, so that even if they don't fill bounds, they can + // look organized when placed together in a row. + tableRow.setGravity(Gravity.CENTER_VERTICAL); for (WidgetItem widgetItem : widgetItems) { WidgetCell widgetCell = addItemCell(tableRow); - widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale); + widgetCell.applyFromCellItem(widgetItem); widgetCell.showAppIconInWidgetTitle(true); widgetCell.showBadge(); if (enableCategorizedWidgetSuggestions()) { widgetCell.showDescription(false); + widgetCell.showDimensions(false); } } addView(tableRow); @@ -144,58 +136,32 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { return widget; } - private RecommendationTableData fitRecommendedWidgetsToTableSpace( - float previewScale, - DeviceProfile deviceProfile, - List> recommendedWidgetsInTable) { - if (previewScale < MAX_DOWN_SCALE_RATIO) { - Log.w(TAG, "Hide recommended widgets. Can't down scale previews to " + previewScale); - return new RecommendationTableData(List.of(), previewScale); - } + private List> selectRowsThatFitInAvailableHeight( + List> recommendedWidgets, @Px float recommendationTableMaxHeight, + DeviceProfile deviceProfile) { + List> filteredRows = new ArrayList<>(); // A naive estimation of the widgets recommendation table height without inflation. float totalHeight = mWidgetsRecommendationTableVerticalPadding; - for (int i = 0; i < recommendedWidgetsInTable.size(); i++) { - List widgetItems = recommendedWidgetsInTable.get(i); + + for (int i = 0; i < recommendedWidgets.size(); i++) { + List widgetItems = recommendedWidgets.get(i); float rowHeight = 0; for (int j = 0; j < widgetItems.size(); j++) { WidgetItem widgetItem = widgetItems.get(j); - Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, - widgetItem); - float previewHeight = widgetSize.getHeight() * previewScale; - rowHeight = Math.max(rowHeight, - previewHeight + mWidgetCellTextViewsHeight + mWidgetCellVerticalPadding); + WidgetPreviewContainerSize previewContainerSize = + WidgetPreviewContainerSize.Companion.forItem(widgetItem, deviceProfile); + float widgetItemHeight = getWidgetSizePx(deviceProfile, previewContainerSize.spanX, + previewContainerSize.spanY).getHeight(); + rowHeight = max(rowHeight, + widgetItemHeight + mWidgetCellTextViewsHeight + mWidgetCellVerticalPadding); + } + if (totalHeight + rowHeight <= recommendationTableMaxHeight) { + totalHeight += rowHeight; + filteredRows.add(new ArrayList<>(widgetItems)); } - totalHeight += rowHeight; } - if (totalHeight < mRecommendationTableMaxHeight) { - return new RecommendationTableData(recommendedWidgetsInTable, previewScale); - } - - if (recommendedWidgetsInTable.size() > 1) { - // We don't want to scale down widgets preview unless we really need to. Reduce the - // num of row by 1 to see if it fits. - return fitRecommendedWidgetsToTableSpace( - previewScale, - deviceProfile, - recommendedWidgetsInTable.subList(/* fromIndex= */0, - /* toIndex= */recommendedWidgetsInTable.size() - 1)); - } - - float nextPreviewScale = previewScale * DOWN_SCALE_RATIO; - return fitRecommendedWidgetsToTableSpace(nextPreviewScale, deviceProfile, - recommendedWidgetsInTable); - } - - /** Data class for the widgets recommendation table and widgets preview scaling. */ - private class RecommendationTableData { - private final List> mRecommendationTable; - private final float mPreviewScale; - - RecommendationTableData(List> recommendationTable, - float previewScale) { - mRecommendationTable = recommendationTable; - mPreviewScale = previewScale; - } + // Perform re-ordering once we have filtered out recommendations that fit. + return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList(); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 165b2feb62..c3bb993baf 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -167,7 +167,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { @Override public void onWidgetsBound() { super.onWidgetsBound(); - if (!mHasRecommendedWidgets && mSelectedHeader == null) { + if (mRecommendedWidgetsCount == 0 && mSelectedHeader == null) { mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop(); } @@ -177,7 +177,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { public void onRecommendedWidgetsBound() { super.onRecommendedWidgetsBound(); - if (mSuggestedWidgetsContainer == null && mHasRecommendedWidgets) { + if (mSuggestedWidgetsContainer == null && mRecommendedWidgetsCount > 0) { setupSuggestedWidgets(LayoutInflater.from(getContext())); mSuggestedWidgetsHeader.callOnClick(); } @@ -209,8 +209,9 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { packageItemInfo.title = suggestionsHeaderTitle; WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create( packageItemInfo, - suggestionsHeaderTitle, - mActivityContext.getPopupDataProvider().getRecommendedWidgets()) + /*titleSectionName=*/ suggestionsHeaderTitle, + /*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(), + /*visibleWidgetsCount=*/ mRecommendedWidgetsCount) .withWidgetListShown(); mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry); @@ -233,7 +234,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { @Override @Px protected float getMaxTableHeight(@Px float noWidgetsViewHeight) { - return Float.MAX_VALUE; + return mContent.getMeasuredHeight() - measureHeightWithVerticalMargins(mHeaderTitle); } @Override diff --git a/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSize.kt b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSize.kt new file mode 100644 index 0000000000..a0414ba136 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSize.kt @@ -0,0 +1,91 @@ +/* + * 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.picker.util + +import com.android.launcher3.DeviceProfile +import com.android.launcher3.model.WidgetItem +import kotlin.math.abs + +/** Size of a preview container in terms of the grid spans. */ +data class WidgetPreviewContainerSize(@JvmField val spanX: Int, @JvmField val spanY: Int) { + companion object { + /** + * Returns the size of the preview container in which the given widget's preview should be + * displayed (by scaling it if necessary). + */ + fun forItem(item: WidgetItem, dp: DeviceProfile): WidgetPreviewContainerSize { + val sizes = + if (dp.isTablet && !dp.isTwoPanels) { + TABLET_WIDGET_PREVIEW_SIZES + } else { + HANDHELD_WIDGET_PREVIEW_SIZES + } + + for ((index, containerSize) in sizes.withIndex()) { + if (containerSize.spanX == item.spanX && containerSize.spanY == item.spanY) { + return containerSize // Exact match! + } + if (containerSize.spanX <= item.spanX && containerSize.spanY <= item.spanY) { + return findClosestFittingContainer( + containerSizes = sizes.toList(), + startIndex = index, + item = item + ) + } + } + // Use largest container if no match found + return sizes.elementAt(0) + } + + private fun findClosestFittingContainer( + containerSizes: List, + startIndex: Int, + item: WidgetItem + ): WidgetPreviewContainerSize { + // Checks if it's a smaller container, but close enough to keep the down-scale minimal. + fun hasAcceptableSize(currentIndex: Int): Boolean { + val container = containerSizes[currentIndex] + val isSmallerThanItem = + container.spanX <= item.spanX && container.spanY <= item.spanY + val isCloseToItemSize = + (item.spanY - container.spanY <= 1) && (item.spanX - container.spanX <= 1) + + return isSmallerThanItem && isCloseToItemSize + } + + var currentIndex = startIndex + var match = containerSizes[currentIndex] + val itemCellSizeRatio = item.spanX.toFloat() / item.spanY + var lastCellSizeRatioDiff = Float.MAX_VALUE + + // Look for a smaller container (up to an acceptable extent) with closest cell size + // ratio. + while (currentIndex <= containerSizes.lastIndex && hasAcceptableSize(currentIndex)) { + val current = containerSizes[currentIndex] + val currentCellSizeRatio = current.spanX.toFloat() / current.spanY + val currentCellSizeRatioDiff = abs(itemCellSizeRatio - currentCellSizeRatio) + + if (currentCellSizeRatioDiff < lastCellSizeRatioDiff) { + lastCellSizeRatioDiff = currentCellSizeRatioDiff + match = containerSizes[currentIndex] + } + currentIndex++ + } + return match + } + } +} diff --git a/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt new file mode 100644 index 0000000000..a016676320 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizes.kt @@ -0,0 +1,52 @@ +/* + * 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.picker.util + +/** + * An ordered list of recommended sizes for the preview containers in handheld devices. + * + * Size of the preview container in which a widget's preview can be displayed. + */ +val HANDHELD_WIDGET_PREVIEW_SIZES: List = + listOf( + WidgetPreviewContainerSize(spanX = 4, spanY = 3), + WidgetPreviewContainerSize(spanX = 4, spanY = 2), + WidgetPreviewContainerSize(spanX = 2, spanY = 3), + WidgetPreviewContainerSize(spanX = 2, spanY = 2), + WidgetPreviewContainerSize(spanX = 4, spanY = 1), + WidgetPreviewContainerSize(spanX = 2, spanY = 1), + WidgetPreviewContainerSize(spanX = 1, spanY = 1), + ) + +/** + * An ordered list of recommended sizes for the preview containers in tablet devices (with larger + * grids). + * + * Size of the preview container in which a widget's preview can be displayed (by scaling the + * preview if necessary). + */ +val TABLET_WIDGET_PREVIEW_SIZES: List = + listOf( + WidgetPreviewContainerSize(spanX = 3, spanY = 4), + WidgetPreviewContainerSize(spanX = 3, spanY = 3), + WidgetPreviewContainerSize(spanX = 3, spanY = 2), + WidgetPreviewContainerSize(spanX = 2, spanY = 3), + WidgetPreviewContainerSize(spanX = 2, spanY = 2), + WidgetPreviewContainerSize(spanX = 3, spanY = 1), + WidgetPreviewContainerSize(spanX = 2, spanY = 1), + WidgetPreviewContainerSize(spanX = 1, spanY = 1), + ) diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java index 74d306276f..5e0e203074 100644 --- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java +++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java @@ -16,11 +16,13 @@ package com.android.launcher3.widget.util; import android.content.Context; +import android.util.Size; import androidx.annotation.Px; import com.android.launcher3.DeviceProfile; import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize; import java.util.ArrayList; import java.util.Comparator; @@ -33,8 +35,8 @@ public final class WidgetsTableUtils { /** * Groups widgets in the following order: * 1. Widgets always go before shortcuts. - * 2. Widgets with smaller horizontal spans will be shown first. - * 3. If widgets have the same horizontal spans, then widgets with a smaller vertical spans will + * 2. Widgets with smaller vertical spans will be shown first. + * 3. If widgets have the same vertical spans, then widgets with a smaller horizontal spans will * go first. * 4. If both widgets have the same horizontal and vertical spans, they will use the same order * from the given {@code widgetItems}. @@ -43,13 +45,28 @@ public final class WidgetsTableUtils { if (item.widgetInfo != null && otherItem.widgetInfo == null) return -1; if (item.widgetInfo == null && otherItem.widgetInfo != null) return 1; - if (item.spanX == otherItem.spanX) { - if (item.spanY == otherItem.spanY) return 0; - return item.spanY > otherItem.spanY ? 1 : -1; + if (item.spanY == otherItem.spanY) { + if (item.spanX == otherItem.spanX) return 0; + return item.spanX > otherItem.spanX ? 1 : -1; } - return item.spanX > otherItem.spanX ? 1 : -1; + return item.spanY > otherItem.spanY ? 1 : -1; }; + /** + * Comparator that enables displaying rows in increasing order of their size (totalW * H); + * except for shortcuts which always show at the bottom. + */ + public static final Comparator> WIDGETS_TABLE_ROW_SIZE_COMPARATOR = + Comparator.comparingInt(row -> { + if (row.stream().anyMatch(WidgetItem::isShortcut)) { + return Integer.MAX_VALUE; + } else { + int rowWidth = row.stream().mapToInt(w -> w.spanX).sum(); + int rowHeight = row.get(0).spanY; + return (rowWidth * rowHeight); + } + }); + /** * Groups {@code widgetItems} items into a 2D array which matches their appearance in a UI * table. This takes liberty to rearrange widgets to make the table visually appealing. @@ -59,72 +76,70 @@ public final class WidgetsTableUtils { final @Px int rowPx, final @Px int cellPadding) { List sortedWidgetItems = widgetItems.stream().sorted(WIDGET_SHORTCUT_COMPARATOR) .collect(Collectors.toList()); - return groupWidgetItemsUsingRowPxWithoutReordering(sortedWidgetItems, context, dp, rowPx, + List> rows = groupWidgetItemsUsingRowPxWithoutReordering( + sortedWidgetItems, context, dp, rowPx, cellPadding); + return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList(); } /** * Groups {@code widgetItems} into a 2D array which matches their appearance in a UI table while * maintaining their order. This function is a variant of - * {@code groupWidgetItemsIntoTableWithoutReordering} in that this uses widget pixels for - * calculation. + * {@code groupWidgetItemsIntoTableWithoutReordering} in that this uses widget container's + * pixels for calculation. * *

Grouping: * 1. Widgets and shortcuts never group together in the same row. - * 2. The ordered widgets are grouped together in the same row until their individual occupying - * pixels exceed the total allowed pixels for the cell. + * 2. Widgets are grouped together only if they have same preview container size. + * 3. Widgets are grouped together in the same row until the total of individual container sizes + * exceed the total allowed pixels for the row. * 3. The ordered shortcuts are grouped together in the same row until their individual * occupying pixels exceed the total allowed pixels for the cell. * 4. If there is only one widget in a row, its width may exceed the {@code rowPx}. * - *

Let's say the {@code rowPx} is set to 600 and we have 5 widgets. Widgets can be grouped - * in the same row if each of their individual occupying pixels does not exceed - * {@code rowPx} / 5 - 2 * {@code cellPadding}. - * Example 1: Row 1: 200x200, 200x300, 100x100. Average horizontal pixels is 200 and no widgets - * exceed that width. This is okay. - * Example 2: Row 1: 200x200, 400x300, 100x100. Average horizontal pixels is 200 and one widget - * exceed that width. This is not allowed. - * Example 3: Row 1: 700x400. This is okay because this is the only item in the row. + *

See WidgetTableUtilsTest */ public static List> groupWidgetItemsUsingRowPxWithoutReordering( List widgetItems, Context context, final DeviceProfile dp, final @Px int rowPx, final @Px int cellPadding) { - List> widgetItemsTable = new ArrayList<>(); ArrayList widgetItemsAtRow = null; + // A row displays only items of same container size. + WidgetPreviewContainerSize containerSizeForRow = null; + @Px int currentRowWidth = 0; + for (WidgetItem widgetItem : widgetItems) { if (widgetItemsAtRow == null) { widgetItemsAtRow = new ArrayList<>(); widgetItemsTable.add(widgetItemsAtRow); } int numOfWidgetItems = widgetItemsAtRow.size(); - @Px int individualSpan = (rowPx / (numOfWidgetItems + 1)) - (2 * cellPadding); + + WidgetPreviewContainerSize containerSize = + WidgetPreviewContainerSize.Companion.forItem(widgetItem, dp); + Size containerSizePx = WidgetSizes.getWidgetSizePx(dp, containerSize.spanX, + containerSize.spanY); + @Px int containerWidth = containerSizePx.getWidth() + (2 * cellPadding); + if (numOfWidgetItems == 0) { widgetItemsAtRow.add(widgetItem); - } else if ( - // Since the size of the widget cell is determined by dividing the maximum span - // pixels evenly, making sure that each widget would have enough span pixels to - // show their contents. - widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1)) - && widgetItemsAtRow.stream().allMatch( - item -> WidgetSizes.getWidgetItemSizePx(context, dp, item) - .getWidth() <= individualSpan) - && WidgetSizes.getWidgetItemSizePx(context, dp, widgetItem) - .getWidth() <= individualSpan) { + containerSizeForRow = containerSize; + currentRowWidth = containerWidth; + } else if ((currentRowWidth + containerWidth) <= rowPx + && widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1)) + && containerSize.equals(containerSizeForRow)) { // Group items in the same row if // 1. they are with the same type, i.e. a row can only have widgets or shortcuts but // never a mix of both. - // 2. Each widget will have horizontal cell span pixels that is at least as large as - // it is required to fit in the horizontal content, unless the widget horizontal - // span pixels is larger than the maximum allowed. - // If an item has horizontal span pixels larger than the maximum allowed pixels - // per row, we just place it in its own row regardless of the horizontal span - // limit. + // 2. Each widget in the given row has same preview container size. widgetItemsAtRow.add(widgetItem); + currentRowWidth += containerWidth; } else { widgetItemsAtRow = new ArrayList<>(); widgetItemsTable.add(widgetItemsAtRow); widgetItemsAtRow.add(widgetItem); + containerSizeForRow = containerSize; + currentRowWidth = containerWidth; } } return widgetItemsTable; diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java index efde7d863a..90271c1cae 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java @@ -107,6 +107,13 @@ public class ApiWrapper { .authority(context.getPackageName()).build()); } + /** + * Returns an intent which can be used to open Private Space Settings. + */ + public static Intent getPrivateSpaceSettingsIntent(Context context) { + return null; + } + /** * Checks if an activity is flagged as non-resizeable. */ diff --git a/tests/assets/databases/BackupAndRestore/launcher.db b/tests/assets/databases/BackupAndRestore/launcher.db new file mode 100644 index 0000000000..126d166492 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher.db differ diff --git a/tests/assets/databases/BackupAndRestore/launcher_3_by_3.db b/tests/assets/databases/BackupAndRestore/launcher_3_by_3.db new file mode 100644 index 0000000000..6d8cd735b7 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher_3_by_3.db differ diff --git a/tests/assets/databases/BackupAndRestore/launcher_4_by_4.db b/tests/assets/databases/BackupAndRestore/launcher_4_by_4.db new file mode 100644 index 0000000000..00061dddf4 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher_4_by_4.db differ diff --git a/tests/assets/databases/BackupAndRestore/launcher_4_by_5.db b/tests/assets/databases/BackupAndRestore/launcher_4_by_5.db new file mode 100644 index 0000000000..e2e65aaf07 Binary files /dev/null and b/tests/assets/databases/BackupAndRestore/launcher_4_by_5.db differ diff --git a/tests/src/com/android/launcher3/celllayout/CellPosMapperTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellPosMapperTest.java similarity index 100% rename from tests/src/com/android/launcher3/celllayout/CellPosMapperTest.java rename to tests/multivalentTests/src/com/android/launcher3/celllayout/CellPosMapperTest.java diff --git a/tests/src/com/android/launcher3/logging/FileLogTest.java b/tests/multivalentTests/src/com/android/launcher3/logging/FileLogTest.java similarity index 100% rename from tests/src/com/android/launcher3/logging/FileLogTest.java rename to tests/multivalentTests/src/com/android/launcher3/logging/FileLogTest.java diff --git a/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt similarity index 100% rename from tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt rename to tests/multivalentTests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt diff --git a/tests/src/com/android/launcher3/popup/PopupPopulatorTest.java b/tests/multivalentTests/src/com/android/launcher3/popup/PopupPopulatorTest.java similarity index 100% rename from tests/src/com/android/launcher3/popup/PopupPopulatorTest.java rename to tests/multivalentTests/src/com/android/launcher3/popup/PopupPopulatorTest.java diff --git a/tests/multivalentTests/src/com/android/launcher3/util/rule/BackAndRestoreRule.kt b/tests/multivalentTests/src/com/android/launcher3/util/rule/BackAndRestoreRule.kt new file mode 100644 index 0000000000..da96939a2b --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/util/rule/BackAndRestoreRule.kt @@ -0,0 +1,119 @@ +/* + * 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.rule + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherPrefs +import java.io.File +import java.nio.file.Paths +import kotlin.io.path.pathString +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Removes all launcher's DBs from the device and copies the dbs in + * assets/databases/BackupAndRestore to the device. It also set's the needed LauncherPrefs variables + * needed to kickstart a backup and restore. + */ +class BackAndRestoreRule : TestRule { + + private val phoneContext = getInstrumentation().targetContext + + private fun dbBackUp() = File(phoneContext.dataDir.path, "/databasesBackUp") + + private fun dbDirectory() = File(phoneContext.dataDir.path, "/databases") + + private fun isWorkspaceDatabase(rawFileName: String): Boolean { + val fileName = Paths.get(rawFileName).fileName.pathString + return fileName.startsWith("launcher") && fileName.endsWith(".db") + } + + fun getDatabaseFiles() = dbDirectory().listFiles().filter { isWorkspaceDatabase(it.name) } + + /** + * Setting RESTORE_DEVICE would trigger a restore next time the Launcher starts, and we remove + * the widgets and apps ids to prevent issues when loading the database. + */ + private fun setRestoreConstants() { + LauncherPrefs.get(phoneContext) + .put(LauncherPrefs.RESTORE_DEVICE.to(InvariantDeviceProfile.TYPE_MULTI_DISPLAY)) + LauncherPrefs.get(phoneContext) + .remove(LauncherPrefs.OLD_APP_WIDGET_IDS, LauncherPrefs.APP_WIDGET_IDS) + } + + private fun uploadDatabase(dbName: String) { + val file = File(File(getInstrumentation().targetContext.dataDir, "/databases"), dbName) + file.writeBytes( + getInstrumentation() + .context + .assets + .open("databases/BackupAndRestore/$dbName") + .readBytes() + ) + file.setWritable(true, false) + } + + private fun uploadDbs() { + uploadDatabase("launcher.db") + uploadDatabase("launcher_4_by_4.db") + uploadDatabase("launcher_4_by_5.db") + uploadDatabase("launcher_3_by_3.db") + } + + private fun savePreviousState() { + dbBackUp().deleteRecursively() + if (!dbDirectory().renameTo(dbBackUp())) { + throw Exception("Unable to move databases to backup directory") + } + dbDirectory().mkdir() + if (!dbDirectory().exists()) { + throw Exception("Databases directory doesn't exists") + } + } + + private fun restorePreviousState() { + dbDirectory().deleteRecursively() + if (!dbBackUp().renameTo(dbDirectory())) { + throw Exception("Unable to restore backup directory to databases directory") + } + dbBackUp().delete() + } + + fun before() { + savePreviousState() + setRestoreConstants() + uploadDbs() + } + + fun after() { + restorePreviousState() + } + + override fun apply(base: Statement?, description: Description?): Statement = + object : Statement() { + override fun evaluate() { + before() + try { + base?.evaluate() + } finally { + after() + } + } + } +} diff --git a/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt similarity index 100% rename from tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt rename to tests/multivalentTests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java index 0907f8fe81..eea4fe5f0c 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java +++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java @@ -47,6 +47,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.pm.UserCache; +import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.ActivityContextWrapper; import com.android.launcher3.util.UserIconInfo; import com.android.launcher3.util.rule.TestStabilityRule; @@ -176,17 +177,15 @@ public class PrivateProfileManagerTest { } @Test - public void openPrivateSpaceSettings_triggersSecurityAndPrivacyIntent() { - Intent expectedIntent = PrivateProfileManager.PRIVATE_SPACE_INTENT; + public void openPrivateSpaceSettings_triggersCorrectIntent() { + Intent expectedIntent = ApiWrapper.getPrivateSpaceSettingsIntent(mContext); ArgumentCaptor acIntent = ArgumentCaptor.forClass(Intent.class); mPrivateProfileManager.setPrivateSpaceSettingsAvailable(true); mPrivateProfileManager.openPrivateSpaceSettings(); Mockito.verify(mContext).startActivity(acIntent.capture()); - Intent actualIntent = acIntent.getValue(); - assertEquals("Intent Action is different", expectedIntent.getAction(), - actualIntent.getAction()); + assertEquals("Intent Action is different", expectedIntent, acIntent.getValue()); } private static void awaitTasksCompleted() throws Exception { diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt new file mode 100644 index 0000000000..3e36bbbcd6 --- /dev/null +++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt @@ -0,0 +1,70 @@ +/* + * 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.backuprestore + +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.Flags +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.model.ModelDbController +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.TestUtil +import com.android.launcher3.util.rule.BackAndRestoreRule +import com.android.launcher3.util.rule.setFlags +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Makes sure to test {@code RestoreDbTask#removeOldDBs}, we need to remove all the dbs that are not + * the last one used when we restore the device. + */ +@RunWith(AndroidJUnit4::class) +@MediumTest +class BackupAndRestoreDBSelectionTest { + + @JvmField @Rule var backAndRestoreRule = BackAndRestoreRule() + + @JvmField + @Rule + val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + @Before + fun setUp() { + setFlagsRule.setFlags(true, Flags.FLAG_NARROW_GRID_RESTORE) + } + + @Test + fun oldDatabasesNotPresentAfterRestore() { + val dbController = ModelDbController(getInstrumentation().targetContext) + dbController.tryMigrateDB(null) + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + assert(backAndRestoreRule.getDatabaseFiles().size == 1) { + "There should only be one database after restoring, the last one used. Actual databases ${backAndRestoreRule.getDatabaseFiles()}" + } + assert( + !LauncherPrefs.get(getInstrumentation().targetContext) + .has(LauncherPrefs.RESTORE_DEVICE) + ) { + "RESTORE_DEVICE shouldn't be present after a backup and restore." + } + } + } +} diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetImageViewTest.kt b/tests/src/com/android/launcher3/widget/picker/WidgetImageViewTest.kt new file mode 100644 index 0000000000..6e751e0c51 --- /dev/null +++ b/tests/src/com/android/launcher3/widget/picker/WidgetImageViewTest.kt @@ -0,0 +1,99 @@ +/* + * 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.picker + +import android.content.Context +import android.graphics.Rect +import android.graphics.drawable.Drawable +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.android.launcher3.util.ActivityContextWrapper +import com.android.launcher3.widget.WidgetImageView +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.spy +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever + +@MediumTest +@RunWith(AndroidJUnit4::class) +class WidgetImageViewTest { + private lateinit var context: Context + private lateinit var widgetImageView: WidgetImageView + + @Mock private lateinit var testDrawable: Drawable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + context = ActivityContextWrapper(ApplicationProvider.getApplicationContext()) + widgetImageView = spy(WidgetImageView(context)) + } + + @Test + fun getBitmapBounds_aspectRatioLargerThanView_scaledByWidth() { + // view - 100 x 100 + whenever(widgetImageView.width).thenReturn(100) + whenever(widgetImageView.height).thenReturn(100) + // bitmap - 200 x 100 + whenever(testDrawable.intrinsicWidth).thenReturn(200) + whenever(testDrawable.intrinsicHeight).thenReturn(100) + + widgetImageView.drawable = testDrawable + val bitmapBounds = widgetImageView.bitmapBounds + + // new scaled width of bitmap is = 100, and height is scaled to 1/2 = 50 + assertThat(bitmapBounds).isEqualTo(Rect(0, 25, 100, 75)) + } + + @Test + fun getBitmapBounds_aspectRatioSmallerThanView_scaledByHeight() { + // view - 100 x 100 + whenever(widgetImageView.width).thenReturn(100) + whenever(widgetImageView.height).thenReturn(100) + // bitmap - 100 x 200 + whenever(testDrawable.intrinsicWidth).thenReturn(100) + whenever(testDrawable.intrinsicHeight).thenReturn(200) + widgetImageView.drawable = testDrawable + + val bitmapBounds = widgetImageView.bitmapBounds + + // new scaled height of bitmap is = 100, and width is scaled to 1/2 = 50 + assertThat(bitmapBounds).isEqualTo(Rect(25, 0, 75, 100)) + } + + @Test + fun getBitmapBounds_noScale_returnsOriginalDrawableBounds() { + // view - 200 x 100 + whenever(widgetImageView.width).thenReturn(200) + whenever(widgetImageView.height).thenReturn(100) + // bitmap - 200 x 100 + whenever(testDrawable.intrinsicWidth).thenReturn(200) + whenever(testDrawable.intrinsicHeight).thenReturn(100) + + widgetImageView.drawable = testDrawable + val bitmapBounds = widgetImageView.bitmapBounds + + // no scaling + assertThat(bitmapBounds).isEqualTo(Rect(0, 0, 200, 100)) + } +} diff --git a/tests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt b/tests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt new file mode 100644 index 0000000000..040fbf5739 --- /dev/null +++ b/tests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt @@ -0,0 +1,154 @@ +/* + * 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.picker.util + +import android.content.ComponentName +import android.content.Context +import android.graphics.Point +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.DeviceProfile +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherAppState +import com.android.launcher3.icons.IconCache +import com.android.launcher3.model.WidgetItem +import com.android.launcher3.util.ActivityContextWrapper +import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WidgetPreviewContainerSizesTest { + private lateinit var context: Context + private lateinit var deviceProfile: DeviceProfile + private lateinit var testInvariantProfile: InvariantDeviceProfile + + @Mock private lateinit var iconCache: IconCache + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context = ActivityContextWrapper(ApplicationProvider.getApplicationContext()) + testInvariantProfile = LauncherAppState.getIDP(context) + deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context) + } + + @Test + fun widgetPreviewContainerSize_forItem_returnsCorrectContainerSize() { + val testSizes = getTestSizes(deviceProfile) + val expectedPreviewContainers = testSizes.values.toList() + + for ((index, widgetSize) in testSizes.keys.withIndex()) { + val widgetItem = createWidgetItem(widgetSize, context, testInvariantProfile, iconCache) + + assertWithMessage("size for $widgetSize should be: ${expectedPreviewContainers[index]}") + .that(WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile)) + .isEqualTo(expectedPreviewContainers[index]) + } + } + + companion object { + private const val TEST_PACKAGE = "com.google.test" + + private val HANDHELD_TEST_SIZES: Map = + mapOf( + // 1x1 + Point(1, 1) to WidgetPreviewContainerSize(1, 1), + // 2x1 + Point(2, 1) to WidgetPreviewContainerSize(2, 1), + Point(3, 1) to WidgetPreviewContainerSize(2, 1), + // 4x1 + Point(4, 1) to WidgetPreviewContainerSize(4, 1), + // 2x2 + Point(2, 2) to WidgetPreviewContainerSize(2, 2), + Point(3, 3) to WidgetPreviewContainerSize(2, 2), + Point(3, 2) to WidgetPreviewContainerSize(2, 2), + // 2x3 + Point(2, 3) to WidgetPreviewContainerSize(2, 3), + Point(3, 4) to WidgetPreviewContainerSize(2, 3), + Point(3, 5) to WidgetPreviewContainerSize(2, 3), + // 4x2 + Point(4, 2) to WidgetPreviewContainerSize(4, 2), + // 4x3 + Point(4, 3) to WidgetPreviewContainerSize(4, 3), + Point(4, 4) to WidgetPreviewContainerSize(4, 3), + ) + + private val TABLET_TEST_SIZES: Map = + mapOf( + // 1x1 + Point(1, 1) to WidgetPreviewContainerSize(1, 1), + // 2x1 + Point(2, 1) to WidgetPreviewContainerSize(2, 1), + // 3x1 + Point(3, 1) to WidgetPreviewContainerSize(3, 1), + Point(4, 1) to WidgetPreviewContainerSize(3, 1), + // 2x2 + Point(2, 2) to WidgetPreviewContainerSize(2, 2), + // 2x3 + Point(2, 3) to WidgetPreviewContainerSize(2, 3), + // 3x2 + Point(3, 2) to WidgetPreviewContainerSize(3, 2), + Point(4, 2) to WidgetPreviewContainerSize(3, 2), + Point(5, 2) to WidgetPreviewContainerSize(3, 2), + // 3x3 + Point(3, 3) to WidgetPreviewContainerSize(3, 3), + Point(4, 4) to WidgetPreviewContainerSize(3, 3), + // 3x4 + Point(5, 4) to WidgetPreviewContainerSize(3, 4), + Point(3, 4) to WidgetPreviewContainerSize(3, 4), + Point(5, 5) to WidgetPreviewContainerSize(3, 4), + Point(6, 4) to WidgetPreviewContainerSize(3, 4), + Point(6, 5) to WidgetPreviewContainerSize(3, 4), + ) + + private fun getTestSizes(dp: DeviceProfile) = + if (dp.isTablet && !dp.isTwoPanels) { + TABLET_TEST_SIZES + } else { + HANDHELD_TEST_SIZES + } + + private fun createWidgetItem( + widgetSize: Point, + context: Context, + invariantDeviceProfile: InvariantDeviceProfile, + iconCache: IconCache + ): WidgetItem { + val providerInfo = + createAppWidgetProviderInfo( + ComponentName.createRelative( + TEST_PACKAGE, + /*cls=*/ ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y + ) + ) + val widgetInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo).apply { + spanX = widgetSize.x + spanY = widgetSize.y + } + return WidgetItem(widgetInfo, invariantDeviceProfile, iconCache, context) + } + } +} diff --git a/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java index 2c5a39621a..b2cb26613d 100644 --- a/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java +++ b/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java @@ -63,6 +63,7 @@ public final class WidgetsTableUtilsTest { private static final String TEST_PACKAGE = "com.google.test"; private static final int SPACE_SIZE = 10; + // Note - actual widget size includes SPACE_SIZE (border) + cell padding. private static final int CELL_SIZE = 50; private static final int NUM_OF_COLS = 5; private static final int NUM_OF_ROWS = 5; @@ -105,7 +106,7 @@ public final class WidgetsTableUtilsTest { @Test - public void groupWidgetItemsIntoTableWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding0_shouldGroupWidgetsInTable() { + public void groupWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding0() { List widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4, mWidget2x2); @@ -113,17 +114,20 @@ public final class WidgetsTableUtilsTest { WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext, mTestDeviceProfile, 220, 0); - // Row 0: 1x1(50px), 2x2(110px) - // Row 1: 2x3(110px), 2x4(110px) - // Row 2: 4x4(230px) - assertThat(widgetItemInTable).hasSize(3); - assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2); - assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4); - assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4); + // With reordering, rows displayed in order of increasing size. + // Row 0: 1x1(50px) + // Row 1: 2x2(in a 2x2 container - 110px) + // Row 2: 2x3(in a 2x3 container - 110px), 2x4(in a 2x3 container - 110px) + // Row 3: 4x4(in a 3x3 container in tablet - 170px; 4x3 on phone - 230px) + assertThat(widgetItemInTable).hasSize(4); + assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1); + assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2); + assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4); + assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4); } @Test - public void groupWidgetItemsIntoTableWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding10_shouldGroupWidgetsInTable() { + public void groupWithReordering_widgetsOnly_maxSpanPxPerRow220_cellPadding10() { List widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4, mWidget2x2); @@ -131,9 +135,13 @@ public final class WidgetsTableUtilsTest { WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext, mTestDeviceProfile, 220, 10); - // Row 0: 1x1(50px), 2x2(110px) - // Row 1: 2x3(110px), 2x4(110px) - // Row 2: 4x4(230px) + // With reordering, but space taken up by cell padding, so, no grouping (even if 2x2 and 2x3 + // use same preview container). + // Row 0: 1x1(50px) + // Row 1: 2x2(in a 2x2 container: 130px) + // Row 2: 2x3(in a 2x3 container: 130px) + // Row 3: 2x4(in a 2x3 container: 130px) + // Row 4: 4x4(in a 3x3 container in tablet - 190px; 4x3 on phone - 250px) assertThat(widgetItemInTable).hasSize(5); assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1); assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2); @@ -143,7 +151,29 @@ public final class WidgetsTableUtilsTest { } @Test - public void groupWidgetItemsIntoTableWithReordering_widgetsOnly_maxSpanPxPerRow350_cellPadding0_shouldGroupWidgetsInTable() { + public void groupWithReordering_widgetsOnly_maxSpanPxPerRow260_cellPadding10() { + List widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4, + mWidget2x2); + + List> widgetItemInTable = + WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext, + mTestDeviceProfile, 260, 10); + + // With reordering, even with cellPadding, enough space to group 2x3 and 2x4 (which also use + // same container) + // Row 0: 1x1(50px) + // Row 1: 2x2(in a 2x2 container: 130px) + // Row 2: 2x3(in a 2x3 container: 130px), 2x4(in a 2x3 container: 130px) + // Row 3: 4x4(in a 3x3 container in tablet - 190px; 4x3 on phone - 250px) + assertThat(widgetItemInTable).hasSize(4); + assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1); + assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2); + assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4); + assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4); + } + + @Test + public void groupWithReordering_widgetsOnly_maxSpanPxPerRow350_cellPadding0() { List widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4, mWidget2x2); @@ -151,17 +181,20 @@ public final class WidgetsTableUtilsTest { WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext, mTestDeviceProfile, 350, 0); - // Row 0: 1x1(50px), 2x2(110px), 2x3(110px) - // Row 1: 2x4(110px) - // Row 2: 4x4(230px) - assertThat(widgetItemInTable).hasSize(3); - assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3); - assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x4); - assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4); + // With reordering, rows displayed in order of increasing size. + // Row 0: 1x1(50px) + // Row 1: 2x2(in a 2x2 container: 110px) + // Row 2: 2x3(in a 2x3 container: 110px), 2x4(in a 2x3 container: 110px) + // Row 3: 4x4(in a 3x3 container in tablet - 170px; 4x3 on phone - 230px) + assertThat(widgetItemInTable).hasSize(4); + assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1); + assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2); + assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4); + assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4); } @Test - public void groupWidgetItemsIntoTableWithReordering_mixItems_maxSpanPxPerRow350_cellPadding0_shouldGroupWidgetsInTable() { + public void groupWithReordering_mixItems_maxSpanPxPerRow350_cellPadding0() { List widgetItems = List.of(mWidget4x4, mShortcut3, mWidget2x3, mShortcut1, mWidget1x1, mShortcut2, mWidget2x4, mWidget2x2); @@ -169,19 +202,22 @@ public final class WidgetsTableUtilsTest { WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering(widgetItems, mContext, mTestDeviceProfile, 350, 0); - // Row 0: 1x1(50px), 2x2(110px), 2x3(110px) - // Row 1: 2x4(110px), - // Row 2: 4x4(230px) - // Row 3: shortcut3(50px), shortcut1(50px), shortcut2(50px) - assertThat(widgetItemInTable).hasSize(4); - assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3); - assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x4); - assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4); - assertThat(widgetItemInTable.get(3)).containsExactly(mShortcut3, mShortcut2, mShortcut1); + // With reordering - rows displays in order of increasing size: + // Row 0: 1x1(50px) + // Row 1: 2x2(110px) + // Row 2: 2x3 (in a 2x3 container 110px), 2x4 (in a 2x3 container 110px) + // Row 3: 4x4 (in a 3x3 container in tablet - 170px; 4x3 on phone - 230px) + // Row 4: shortcut3, shortcut1, shortcut2 (shortcuts are always displayed at bottom) + assertThat(widgetItemInTable).hasSize(5); + assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1); + assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x2); + assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x3, mWidget2x4); + assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4); + assertThat(widgetItemInTable.get(4)).containsExactly(mShortcut3, mShortcut2, mShortcut1); } @Test - public void groupWidgetItemsIntoTableWithoutReordering_maxSpanPxPerRow220_cellPadding0_shouldMaintainTheOrder() { + public void groupWithoutReordering_maxSpanPxPerRow220_cellPadding0() { List widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4, mWidget2x2); @@ -189,13 +225,19 @@ public final class WidgetsTableUtilsTest { WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering(widgetItems, mContext, mTestDeviceProfile, 220, 0); - // Row 0: 4x4(230px) - // Row 1: 2x3(110px), 1x1(50px) - // Row 2: 2x4(110px), 2x2(110px) - assertThat(widgetItemInTable).hasSize(3); + // Without reordering, widgets are grouped only if the next one fits and uses same preview + // container: + // Row 0: 4x4(in a 3x3 container in tablet - 170px; 4x3 on phone - 230px) + // Row 1: 2x3(in a 2x3 container - 110px) + // Row 2: 1x1(50px) + // Row 3: 2x4(in a 2x3 container - 110px) + // Row 4: 2x2(in a 2x2 container - 110px) + assertThat(widgetItemInTable).hasSize(5); assertThat(widgetItemInTable.get(0)).containsExactly(mWidget4x4); - assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget1x1); - assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x4, mWidget2x2); + assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3); + assertThat(widgetItemInTable.get(2)).containsExactly(mWidget1x1); + assertThat(widgetItemInTable.get(3)).containsExactly(mWidget2x4); + assertThat(widgetItemInTable.get(4)).containsExactly(mWidget2x2); } private void initDP() {