diff options
26 files changed, 461 insertions, 28 deletions
diff --git a/Android.bp b/Android.bp index 61042f6d70..4354b66840 100644 --- a/Android.bp +++ b/Android.bp @@ -203,6 +203,7 @@ android_library { "animationlib", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", + "android.appwidget.flags-aconfig-java", ], sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index 6d899d90e5..d379132bcf 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -150,6 +150,13 @@ flag { } flag { + name: "enable_generated_previews" + namespace: "launcher" + description: "Enables support for RemoteViews previews in the widget picker." + bug: "306546610" +} + +flag { name: "enable_categorized_widget_suggestions" namespace: "launcher" description: "Enables widget suggestions in widget picker to be displayed in categories" diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml index 5af8d5165f..3256b0b02d 100644 --- a/quickstep/res/layout/keyboard_quick_switch_view.xml +++ b/quickstep/res/layout/keyboard_quick_switch_view.xml @@ -43,7 +43,7 @@ android:layout_height="@dimen/keyboard_quick_switch_no_recent_items_icon_size" android:layout_marginBottom="@dimen/keyboard_quick_switch_no_recent_items_icon_margin" android:src="@drawable/ic_empty_recents" - android:tint="?androidprv:attr/materialColorOnSurfaceInverse" + android:tint="?androidprv:attr/materialColorOnSurface" android:importantForAccessibility="no" app:layout_constraintVertical_chainStyle="packed" diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 325c25506b..14f615e240 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -105,6 +105,8 @@ <string name="back_gesture_feedback_cancelled">Make sure you swipe from the right or left edge to the middle of the screen and let go</string> <!-- Feedback shown after completing the back gesture step if the user is following the full gesture tutorial flow. [CHAR LIMIT=100] --> <string name="back_gesture_feedback_complete_with_overview_follow_up">You learned how to swipe from the right to go back. Next up, learn how to switch apps.</string> + <!-- Feedback shown after completing the back gesture step if the user is following the full gesture tutorial flow. [CHAR LIMIT=100] --> + <string name="back_gesture_feedback_complete_with_follow_up">You completed the go back gesture. Next up, learn how to switch apps.</string> <!-- Feedback shown after completing the back gesture step if the user started this tutorial individually. [CHAR LIMIT=100] --> <string name="back_gesture_feedback_complete_without_follow_up">You completed the go back gesture</string> <!-- Feedback shown during interactive parts of Back gesture tutorial when the gesture is within the nav bar region. [CHAR LIMIT=100] --> diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml index bdc86b217d..350c752c56 100644 --- a/quickstep/res/values/styles.xml +++ b/quickstep/res/values/styles.xml @@ -273,7 +273,7 @@ </style> <style name="KeyboardQuickSwitchText.OnBackground" parent="KeyboardQuickSwitchText"> - <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceInverse</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> </style> <style name="GestureTutorialActivity" parent="@style/AppTheme"> diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index 7a69c556c0..747c9cb308 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -803,6 +803,11 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private void addJankMonitorListener( AnimatorSet animator, boolean expanding, @StashAnimation int animationType) { View v = mControllers.taskbarActivityContext.getDragLayer(); + if (!v.isAttachedToWindow()) { + // If the task bar drag layer is not attached to window, we don't need to monitor jank + // (actually we can't pass in an unattached view either). + return; + } int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND : InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE; animator.addListener(new AnimatorListenerAdapter() { diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java index 404bca9795..6757cd88cc 100644 --- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java @@ -85,7 +85,9 @@ final class BackGestureTutorialController extends TutorialController { public int getSuccessFeedbackSubtitle() { return mTutorialFragment.isAtFinalStep() ? R.string.back_gesture_feedback_complete_without_follow_up - : R.string.back_gesture_feedback_complete_with_overview_follow_up; + : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() + ? R.string.back_gesture_feedback_complete_with_follow_up + : R.string.back_gesture_feedback_complete_with_overview_follow_up; } @Override diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt index 87cbdd1b94..fc508537ab 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt @@ -6,7 +6,6 @@ import android.view.Surface.ROTATION_270 import android.view.Surface.Rotation import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.Space @@ -14,7 +13,9 @@ import androidx.test.runner.AndroidJUnit4 import com.android.launcher3.DeviceProfile import com.android.launcher3.R import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION -import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_CONTEXTUAL_BUTTONS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_NAV_BUTTONS +import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_START_CONTEXTUAL_BUTTONS import com.android.systemui.shared.rotation.RotationButton import java.lang.IllegalStateException import org.junit.Assume.assumeTrue @@ -52,11 +53,11 @@ class NavButtonLayoutFactoryTest { whenever(mockNavLayout.findViewById<View>(R.id.recent_apps)).thenReturn(mockRecentsButton) // Init top level layout - whenever(mockParentButtonContainer.findViewById<LinearLayout>(R.id.end_nav_buttons)) + whenever(mockParentButtonContainer.requireViewById<LinearLayout>(ID_END_NAV_BUTTONS)) .thenReturn(mockNavLayout) - whenever(mockParentButtonContainer.findViewById<ViewGroup>(R.id.end_contextual_buttons)) + whenever(mockParentButtonContainer.requireViewById<ViewGroup>(ID_END_CONTEXTUAL_BUTTONS)) .thenReturn(mockEndContextualLayout) - whenever(mockParentButtonContainer.findViewById<ViewGroup>(R.id.start_contextual_buttons)) + whenever(mockParentButtonContainer.requireViewById<ViewGroup>(ID_START_CONTEXTUAL_BUTTONS)) .thenReturn(mockStartContextualLayout) } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index d124746be7..99fca62ac1 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -441,17 +441,35 @@ public class LauncherModel implements InstallSessionTracker.Callback { @Override public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { + IconCache iconCache = app.getIconCache(); final IntSet removedIds = new IntSet(); + HashSet<WorkspaceItemInfo> archivedItemsToCacheRefresh = new HashSet<>(); + HashSet<String> archivedPackagesToCacheRefresh = new HashSet<>(); synchronized (dataModel) { for (ItemInfo info : dataModel.itemsIdMap) { if (info instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) info).hasPromiseIconUi() && user.equals(info.user) - && info.getIntent() != null - && TextUtils.equals(packageName, info.getIntent().getPackage())) { - removedIds.add(info.id); + && info.getIntent() != null) { + if (TextUtils.equals(packageName, info.getIntent().getPackage())) { + removedIds.add(info.id); + } + if (((WorkspaceItemInfo) info).isArchived()) { + WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info; + // Remove package cache icon for archived app in case of a session + // failure. + mApp.getIconCache().removeIconsForPkg(packageName, user); + // Refresh icons on the workspace for archived apps. + iconCache.getTitleAndIcon(workspaceItem, + workspaceItem.usingLowResIcon()); + archivedPackagesToCacheRefresh.add(packageName); + archivedItemsToCacheRefresh.add(workspaceItem); + } } } + if (!archivedPackagesToCacheRefresh.isEmpty()) { + apps.updateIconsAndLabels(archivedPackagesToCacheRefresh, user); + } } if (!removedIds.isEmpty()) { @@ -459,6 +477,10 @@ public class LauncherModel implements InstallSessionTracker.Callback { ItemInfoMatcher.ofItemIds(removedIds), "removed because install session failed"); } + if (!archivedItemsToCacheRefresh.isEmpty()) { + bindUpdatedWorkspaceItems(archivedItemsToCacheRefresh.stream().toList()); + bindApplicationsIfNeeded(); + } } }); } diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java index 7067fa225b..911612ff19 100644 --- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -71,7 +71,7 @@ public class AllAppsFastScrollHelper { @Override protected int getVerticalSnapPreference() { - return SNAP_TO_START; + return SNAP_TO_ANY; } @Override diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java index 051cf50ccc..009a2aa6b7 100644 --- a/src/com/android/launcher3/allapps/AllAppsStore.java +++ b/src/com/android/launcher3/allapps/AllAppsStore.java @@ -93,11 +93,14 @@ public class AllAppsStore<T extends Context & ActivityContext> { * Sets the current set of apps and sets mapping for {@link PackageUserKey} to Uid for * the current set of apps. * - * <p> Note that shouldPreinflate param should be set to {@code false} for taskbar, because this - * method is too late to preinflate all apps, as user will open all apps in the same frame. + * <p> Note that shouldPreinflate param should be set to {@code false} for taskbar, because + * this method is too late to preinflate all apps, as user will open all apps in the frame + * + * <p>Param: apps are required to be sorted using the comparator COMPONENT_KEY_COMPARATOR + * in order to enable binary search on the mApps store */ public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map, - boolean shouldPreinflate) { + boolean shouldPreinflate) { mApps = apps == null ? EMPTY_ARRAY : apps; mModelFlags = flags; notifyUpdate(); diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java index 311a40ef64..a0867dbaf1 100644 --- a/src/com/android/launcher3/allapps/AppInfoComparator.java +++ b/src/com/android/launcher3/allapps/AppInfoComparator.java @@ -43,9 +43,7 @@ public class AppInfoComparator implements Comparator<AppInfo> { @Override public int compare(AppInfo a, AppInfo b) { // Order by the title in the current locale - int result = mLabelComparator.compare( - a.title == null ? "" : a.title.toString(), - b.title == null ? "" : b.title.toString()); + int result = mLabelComparator.compare(getSortingTitle(a), getSortingTitle(b)); if (result != 0) { return result; } @@ -64,4 +62,14 @@ public class AppInfoComparator implements Comparator<AppInfo> { return aUserSerial.compareTo(bUserSerial); } } + + private String getSortingTitle(AppInfo info) { + if (info.appTitle != null) { + return info.appTitle.toString(); + } + if (info.title != null) { + return info.title.toString(); + } + return ""; + } } diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 2f7f51e6f7..d8fa90ae82 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -219,7 +219,19 @@ public class IconCache extends BaseIconCache { CacheEntry entry = cacheLocked(application.componentName, application.user, () -> null, mLauncherActivityInfoCachingLogic, false, application.usingLowResIcon()); - if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) { + if (entry.bitmap == null || isDefaultIcon(entry.bitmap, application.user)) { + return; + } + + boolean preferPackageIcon = application.isArchived(); + if (preferPackageIcon) { + String packageName = application.getTargetPackage(); + CacheEntry packageEntry = + cacheLocked(new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + application.user, () -> null, mLauncherActivityInfoCachingLogic, + false, application.usingLowResIcon()); + applyPackageEntry(packageEntry, application, entry); + } else { applyCacheEntry(entry, application); } } @@ -227,10 +239,14 @@ public class IconCache extends BaseIconCache { /** * Fill in {@param info} with the icon and label for {@param activityInfo} */ + @SuppressWarnings("NewApi") public synchronized void getTitleAndIcon(ItemInfoWithIcon info, LauncherActivityInfo activityInfo, boolean useLowResIcon) { + boolean isAppArchived = Utilities.enableSupportForArchiving() && activityInfo != null + && activityInfo.getActivityInfo().isArchived; // If we already have activity info, no need to use package icon - getTitleAndIcon(info, () -> activityInfo, false, useLowResIcon); + getTitleAndIcon(info, () -> activityInfo, isAppArchived, useLowResIcon, + isAppArchived); } /** @@ -309,7 +325,7 @@ public class IconCache extends BaseIconCache { } else { Intent intent = info.getIntent(); getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user), - true, useLowResIcon); + true, useLowResIcon, info.isArchived()); } } @@ -334,6 +350,28 @@ public class IconCache extends BaseIconCache { } /** + * Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + @NonNull ItemInfoWithIcon infoInOut, + @NonNull Supplier<LauncherActivityInfo> activityInfoProvider, + boolean usePkgIcon, boolean useLowResIcon, boolean preferPackageEntry) { + CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user, + activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon, + useLowResIcon); + if (preferPackageEntry) { + String packageName = infoInOut.getTargetPackage(); + CacheEntry packageEntry = cacheLocked( + new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + infoInOut.user, activityInfoProvider, mLauncherActivityInfoCachingLogic, + usePkgIcon, useLowResIcon); + applyPackageEntry(packageEntry, infoInOut, entry); + } else { + applyCacheEntry(entry, infoInOut); + } + } + + /** * Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles. * * @param iconRequestInfos List of IconRequestInfos representing titles and icons to query. @@ -551,6 +589,19 @@ public class IconCache extends BaseIconCache { } } + protected void applyPackageEntry(@NonNull final CacheEntry packageEntry, + @NonNull final ItemInfoWithIcon info, @NonNull final CacheEntry fallbackEntry) { + info.title = Utilities.trim(packageEntry.title); + info.appTitle = Utilities.trim(fallbackEntry.title); + info.contentDescription = packageEntry.contentDescription; + info.bitmap = packageEntry.bitmap; + if (packageEntry.bitmap == null) { + // TODO: entry.bitmap can never be null, so this should not happen at all. + Log.wtf(TAG, "Cannot find bitmap from the cache, default icon was loaded."); + info.bitmap = getDefaultIcon(info.user); + } + } + public Drawable getFullResIcon(LauncherActivityInfo info) { return mIconProvider.getIcon(info, mIconDpi); } diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java index c99b8891c8..ddf4023f24 100644 --- a/src/com/android/launcher3/model/WidgetItem.java +++ b/src/com/android/launcher3/model/WidgetItem.java @@ -1,5 +1,9 @@ package com.android.launcher3.model; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX; + import static com.android.launcher3.Utilities.ATLEAST_S; import android.annotation.SuppressLint; @@ -7,13 +11,19 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import androidx.core.os.BuildCompat; +import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.icons.IconCache; import com.android.launcher3.pm.ShortcutConfigActivityInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.WidgetManagerHelper; /** * An wrapper over various items displayed in a widget picker, @@ -28,9 +38,11 @@ public class WidgetItem extends ComponentKey { public final String label; public final CharSequence description; public final int spanX, spanY; + public final SparseArray<RemoteViews> generatedPreviews; public WidgetItem(LauncherAppWidgetProviderInfo info, - InvariantDeviceProfile idp, IconCache iconCache, Context context) { + InvariantDeviceProfile idp, IconCache iconCache, Context context, + WidgetManagerHelper helper) { super(info.provider, info.getProfile()); label = iconCache.getTitleNoCache(info); @@ -40,6 +52,27 @@ public class WidgetItem extends ComponentKey { spanX = Math.min(info.spanX, idp.numColumns); spanY = Math.min(info.spanY, idp.numRows); + + if (BuildCompat.isAtLeastV() && Flags.enableGeneratedPreviews()) { + generatedPreviews = new SparseArray<>(3); + for (int widgetCategory : new int[] { + WIDGET_CATEGORY_HOME_SCREEN, + WIDGET_CATEGORY_KEYGUARD, + WIDGET_CATEGORY_SEARCHBOX, + }) { + if ((widgetCategory & widgetInfo.generatedPreviewCategories) != 0) { + generatedPreviews.put(widgetCategory, + helper.loadGeneratedPreview(widgetInfo, widgetCategory)); + } + } + } else { + generatedPreviews = null; + } + } + + public WidgetItem(LauncherAppWidgetProviderInfo info, + InvariantDeviceProfile idp, IconCache iconCache, Context context) { + this(info, idp, iconCache, context, new WidgetManagerHelper(context)); } public WidgetItem(ShortcutConfigActivityInfo info, IconCache iconCache, PackageManager pm) { @@ -50,6 +83,7 @@ public class WidgetItem extends ComponentKey { widgetInfo = null; activityInfo = info; spanX = spanY = 1; + generatedPreviews = null; } /** @@ -78,4 +112,15 @@ public class WidgetItem extends ComponentKey { public boolean isShortcut() { return activityInfo != null; } + + /** + * Returns whether this {@link WidgetItem} has a generated preview for the given widget + * category. + */ + public boolean hasGeneratedPreview(int widgetCategory) { + if (!Flags.enableGeneratedPreviews() || generatedPreviews == null) { + return false; + } + return generatedPreviews.contains(widgetCategory); + } } diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 86393a0012..55849c2149 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -160,6 +160,13 @@ public class ItemInfo { public CharSequence title; /** + * Optionally set: The appTitle might e.g. be different if {@code title} is used to + * display progress (e.g. Downloading..). + */ + @Nullable + public CharSequence appTitle; + + /** * Content description of the item. */ @Nullable diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index 8f5e2b66e1..c75f9d114e 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -16,6 +16,8 @@ package com.android.launcher3.widget; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN; + import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY; import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx; @@ -44,6 +46,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.CheckLongPressHelper; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.icons.FastBitmapDrawable; @@ -241,6 +244,11 @@ public class WidgetCell extends LinearLayout { mAppWidgetHostViewPreview = createAppWidgetHostView(context); setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, mRemoteViewsPreview); + } else if (Flags.enableGeneratedPreviews() + && item.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)) { + mAppWidgetHostViewPreview = createAppWidgetHostView(context); + setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, + item.generatedPreviews.get(WIDGET_CATEGORY_HOME_SCREEN)); } else if (item.hasPreviewLayout()) { // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview // as a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java index 058523b530..52767a4135 100644 --- a/src/com/android/launcher3/widget/WidgetManagerHelper.java +++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java @@ -24,8 +24,11 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; import android.os.UserHandle; +import android.widget.RemoteViews; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -130,6 +133,23 @@ public class WidgetManagerHelper { appWidgetId).getBoolean(WIDGET_OPTION_RESTORE_COMPLETED); } + + /** + * Load RemoteViews preview for this provider if available. + * + * @param info The provider info for the widget you want to preview. + * @param widgetCategory The widget category for which you want to display previews. + * + * @return Returns the widget preview that matches selected category, if available. + */ + @Nullable + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + public RemoteViews loadGeneratedPreview(@NonNull AppWidgetProviderInfo info, + int widgetCategory) { + if (!android.appwidget.flags.Flags.generatedPreviews()) return null; + return mAppWidgetManager.getWidgetPreview(info.provider, info.getProfile(), widgetCategory); + } + private static Stream<AppWidgetProviderInfo> allWidgetsSteam(Context context) { AppWidgetManager awm = context.getSystemService(AppWidgetManager.class); return Stream.concat( diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java index 4c5bfd8913..8b983fc391 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java @@ -147,7 +147,8 @@ public class WidgetsModel { LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo); widgetsAndShortcuts.add(new WidgetItem( - launcherWidgetInfo, idp, app.getIconCache(), app.getContext())); + launcherWidgetInfo, idp, app.getIconCache(), app.getContext(), + widgetManager)); updatedItems.add(launcherWidgetInfo); } @@ -206,6 +207,7 @@ public class WidgetsModel { public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user, LauncherAppState app) { + WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext()); for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) { if (packageNames.contains(entry.getKey().packageName)) { List<WidgetItem> items = entry.getValue(); @@ -219,7 +221,7 @@ public class WidgetsModel { } else { items.set(i, new WidgetItem(item.widgetInfo, app.getInvariantDeviceProfile(), app.getIconCache(), - app.getContext())); + app.getContext(), widgetManager)); } } } @@ -337,4 +339,4 @@ public class WidgetsModel { return mMap.values(); } } -}
\ No newline at end of file +} diff --git a/tests/Android.bp b/tests/Android.bp index ed8609e3d0..9ce0777623 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -127,6 +127,7 @@ android_library { "testables", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", + "android.appwidget.flags-aconfig-java", ], manifest: "AndroidManifest-common.xml", platform_apis: true, diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 978e1f2f6c..679bd0116d 100644 --- a/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -193,6 +193,7 @@ public abstract class AbstractLauncherUiTest { protected AbstractLauncherUiTest() { mLauncher.enableCheckEventsForSuccessfulGestures(); + mLauncher.setAnomalyChecker(AbstractLauncherUiTest::verifyKeyguardInvisible); try { mDevice.setOrientationNatural(); } catch (RemoteException e) { diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index cdde605f2c..e17994caf5 100644 --- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -204,6 +204,9 @@ public final class LauncherInstrumentation { private LogEventChecker mEventChecker; + // UI anomaly checker provided by the test. + private Runnable mTestAnomalyChecker; + private boolean mCheckEventsForSuccessfulGestures = false; private Runnable mOnLauncherCrashed; @@ -573,8 +576,31 @@ public final class LauncherInstrumentation { checkForAnomaly(false, false); } + /** + * Allows the test to provide a pluggable anomaly checker. It’s supposed to throw an exception + * if the check fails. The test may provide its own anomaly checker, for example, if it wants to + * check for an anomaly that’s recognized by the standard TAPL anomaly checker, but wants a + * custom error message, such as adding information whether the keyguard is seen for the first + * time during the shard execution. + */ + public void setAnomalyChecker(Runnable anomalyChecker) { + mTestAnomalyChecker = anomalyChecker; + } + + /** + * Verifies that there are no visible UI anomalies. An "anomaly" is a state of UI that should + * never happen during the text execution. Anomaly is something different from just “regular” + * unexpected state of the Launcher such as when we see Workspace after swiping up to All Apps. + * Workspace is a normal state. We can contrast this with an anomaly, when, for example, we see + * a lock screen. Launcher tests can never bring the lock screen, so the very presence of the + * lock screen is an indication that something went very wrong, and perhaps is caused by reasons + * outside of the Launcher and its tests, perhaps, by a crash in System UI. Diagnosing anomalies + * helps to understand faster whether the problem is in the Launcher or its tests, or outside. + */ public void checkForAnomaly( boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) { + if (mTestAnomalyChecker != null) mTestAnomalyChecker.run(); + final String systemAnomalyMessage = getSystemAnomalyMessage(ignoreNavmodeChangeStates, ignoreOnlySystemUiViews); if (systemAnomalyMessage != null) { diff --git a/tests/res/layout/test_layout_appwidget_red.xml b/tests/res/layout/test_layout_appwidget_red.xml index 48d3e8107d..0f2bda39a9 100644 --- a/tests/res/layout/test_layout_appwidget_red.xml +++ b/tests/res/layout/test_layout_appwidget_red.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/content" android:orientation="vertical" android:background="#FFFF0000" android:layout_width="match_parent" diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java index 6fce4c65ff..0f23165ebf 100644 --- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java +++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java @@ -25,6 +25,7 @@ import static org.junit.Assume.assumeTrue; import android.content.Intent; import android.platform.test.annotations.PlatinumTest; +import android.platform.test.rule.ScreenRecordRule; import androidx.test.filters.FlakyTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -130,6 +131,7 @@ public class TaplOpenCloseAllAppsTest extends AbstractLauncherUiTest { @Test @PortraitLandscape @PlatinumTest(focusArea = "launcher") + @ScreenRecordRule.ScreenRecord // b/322228038 public void testAllAppsFromHome() { // Test opening all apps assertNotNull("switchToAllApps() returned null", diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index f7710522bb..6c35f68391 100644 --- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -2,21 +2,33 @@ package com.android.launcher3.model; import static android.os.Process.myUserHandle; +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; +import static com.android.launcher3.util.TestUtil.DUMMY_CLASS_NAME; +import static com.android.launcher3.util.TestUtil.DUMMY_PACKAGE; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Context; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.uiautomator.UiDevice; import com.android.launcher3.LauncherAppState; import com.android.launcher3.icons.BitmapInfo; @@ -26,12 +38,15 @@ import com.android.launcher3.util.IntSet; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.LauncherModelHelper; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.util.TestUtil; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -43,9 +58,18 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class CacheDataUpdatedTaskTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String PENDING_APP_1 = TEST_PACKAGE + ".pending1"; private static final String PENDING_APP_2 = TEST_PACKAGE + ".pending2"; + private static final String ARCHIVED_PACKAGE = DUMMY_PACKAGE; + private static final String ARCHIVED_CLASS_NAME = DUMMY_CLASS_NAME; + private static final String ARCHIVED_TITLE = "Aardwolf"; + + private LauncherModelHelper mModelHelper; private Context mContext; @@ -57,6 +81,7 @@ public class CacheDataUpdatedTaskTest { mContext = mModelHelper.sandboxContext; mSession1 = mModelHelper.createInstallerSession(PENDING_APP_1); mModelHelper.createInstallerSession(PENDING_APP_2); + TestUtil.installDummyApp(); LauncherLayoutBuilder builder = new LauncherLayoutBuilder() .atHotseat(1).putFolder("MyFolder") @@ -73,14 +98,22 @@ public class CacheDataUpdatedTaskTest { .addApp(PENDING_APP_2, TEST_ACTIVITY) // 8 .addApp(PENDING_APP_2, TEST_ACTIVITY2) // 9 .addApp(PENDING_APP_2, TEST_ACTIVITY3) // 10 + + // Dummy Test Package + .addApp(ARCHIVED_PACKAGE, ARCHIVED_CLASS_NAME) // 11 .build(); mModelHelper.setupDefaultLayoutProvider(builder); mModelHelper.loadModelSync(); - assertEquals(10, mModelHelper.getBgDataModel().itemsIdMap.size()); + assertEquals(11, mModelHelper.getBgDataModel().itemsIdMap.size()); + + UiDevice device = UiDevice.getInstance(getInstrumentation()); + assertThat(device.executeShellCommand(String.format("pm archive %s", ARCHIVED_PACKAGE))) + .isEqualTo("Success\n"); } @After - public void tearDown() { + public void tearDown() throws IOException { + TestUtil.uninstallDummyApp(); mModelHelper.destroy(); } @@ -138,6 +171,47 @@ public class CacheDataUpdatedTaskTest { }); } + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING) + public void testSessionUpdate_archivedApps_sessionInfoPrioritized() { + // Run on model executor so that no other task runs in the middle. + runOnExecutorSync(MODEL_EXECUTOR, () -> { + // Clear all icons from apps list so that its easy to check what was updated + allItems().forEach(wi -> wi.bitmap = BitmapInfo.LOW_RES_INFO); + int mSession2 = mModelHelper.createInstallerSession(ARCHIVED_PACKAGE); + mModelHelper.getModel().enqueueModelUpdateTask( + newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, ARCHIVED_PACKAGE)); + List<Integer> pendingArchivedAppIds = List.of(11); + // Mark the app items as archived. + allItems().forEach(wi -> { + if (pendingArchivedAppIds.contains(wi.id)) { + wi.runtimeStatusFlags |= FLAG_ARCHIVED; + } + }); + // Before cache is updated with sessionInfo, confirm the title. + for (WorkspaceItemInfo info : allItems()) { + if (pendingArchivedAppIds.contains(info.id)) { + assertEquals(info.title, ARCHIVED_TITLE); + } + } + + // Update the cache with session details. + LauncherAppState.getInstance(mContext).getIconCache().updateSessionCache( + new PackageUserKey(ARCHIVED_PACKAGE, myUserHandle()), + mContext.getPackageManager().getPackageInstaller().getSessionInfo(mSession2)); + + // Trigger a refresh for workspace itemInfo objects. + mModelHelper.getModel().enqueueModelUpdateTask( + newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, ARCHIVED_PACKAGE)); + // Verify the new title from session is applied to the iconInfo. + for (WorkspaceItemInfo info : allItems()) { + if (pendingArchivedAppIds.contains(info.id)) { + assertEquals(info.title, ARCHIVED_PACKAGE); + } + } + }); + } + private void verifyUpdate(int... idsUpdated) { IntSet updates = IntSet.wrap(idsUpdated); for (WorkspaceItemInfo info : allItems()) { diff --git a/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt new file mode 100644 index 0000000000..11855e6a3c --- /dev/null +++ b/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt @@ -0,0 +1,142 @@ +package com.android.launcher3.widget + +import android.appwidget.AppWidgetProviderInfo +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX +import android.content.ComponentName +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.view.LayoutInflater +import android.widget.RemoteViews +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags.FLAG_ENABLE_GENERATED_PREVIEWS +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.icons.IconCache +import com.android.launcher3.icons.IconProvider +import com.android.launcher3.model.WidgetItem +import com.android.launcher3.tests.R +import com.android.launcher3.util.ActivityContextWrapper +import com.android.launcher3.util.Executors +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class GeneratedPreviewTest { + @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private val providerName = + ComponentName( + "com.android.launcher3.tests", + "com.android.launcher3.testcomponent.AppWidgetNoConfig" + ) + private val generatedPreviewLayout = R.layout.test_layout_appwidget_blue + private lateinit var context: Context + private lateinit var generatedPreview: RemoteViews + private lateinit var widgetCell: WidgetCell + private lateinit var helper: WidgetManagerHelper + private lateinit var appWidgetProviderInfo: LauncherAppWidgetProviderInfo + private lateinit var widgetItem: WidgetItem + + @Before + fun setup() { + context = getApplicationContext() + generatedPreview = RemoteViews(context.packageName, generatedPreviewLayout) + widgetCell = + LayoutInflater.from(ActivityContextWrapper(context)) + .inflate(com.android.launcher3.R.layout.widget_cell, null) as WidgetCell + appWidgetProviderInfo = + AppWidgetProviderInfo() + .apply { + generatedPreviewCategories = WIDGET_CATEGORY_HOME_SCREEN + provider = providerName + providerInfo = ActivityInfo().apply { applicationInfo = ApplicationInfo() } + } + .let { LauncherAppWidgetProviderInfo.fromProviderInfo(context, it) } + helper = + object : WidgetManagerHelper(context) { + override fun loadGeneratedPreview( + info: AppWidgetProviderInfo, + widgetCategory: Int + ) = + generatedPreview.takeIf { + info === appWidgetProviderInfo && + widgetCategory == WIDGET_CATEGORY_HOME_SCREEN + } + } + createWidgetItem() + } + + private fun createWidgetItem() { + Executors.MODEL_EXECUTOR.submit { + val idp = InvariantDeviceProfile() + widgetItem = + WidgetItem( + appWidgetProviderInfo, + idp, + IconCache(context, idp, null, IconProvider(context)), + context, + helper, + ) + } + .get() + } + + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS) + fun widgetItem_hasGeneratedPreview() { + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isTrue() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse() + } + + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS) + fun widgetItem_hasGeneratedPreview_noPreview() { + appWidgetProviderInfo.generatedPreviewCategories = 0 + createWidgetItem() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isFalse() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse() + } + + @Test + @RequiresFlagsDisabled(FLAG_ENABLE_GENERATED_PREVIEWS) + fun widgetItem_hasGeneratedPreview_flagDisabled() { + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isFalse() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse() + assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse() + } + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS) + fun widgetItem_getGeneratedPreview() { + val preview = widgetItem.generatedPreviews.get(WIDGET_CATEGORY_HOME_SCREEN) + assertThat(preview).isEqualTo(generatedPreview) + } + + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS) + fun widgetCell_showGeneratedPreview() { + widgetCell.applyFromCellItem(widgetItem) + assertThat(widgetCell.appWidgetHostViewPreview).isNotNull() + assertThat(widgetCell.appWidgetHostViewPreview?.appWidgetInfo) + .isEqualTo(appWidgetProviderInfo) + } + + @Test + @RequiresFlagsDisabled(FLAG_ENABLE_GENERATED_PREVIEWS) + fun widgetCell_showGeneratedPreview_flagDisabled() { + widgetCell.applyFromCellItem(widgetItem) + assertThat(widgetCell.appWidgetHostViewPreview).isNull() + } +} diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java index 60590e75e3..0286279b09 100644 --- a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java +++ b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java @@ -52,6 +52,7 @@ import com.android.launcher3.util.Executors; import com.android.launcher3.util.WidgetUtils; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetManagerHelper; import com.android.launcher3.widget.model.WidgetsListContentEntry; import org.junit.Before; @@ -137,6 +138,7 @@ public final class WidgetsListTableViewHolderBinderTest { } private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) { + WidgetManagerHelper widgetManager = new WidgetManagerHelper(mContext); ArrayList<WidgetItem> widgetItems = new ArrayList<>(); for (int i = 0; i < numOfWidgets; i++) { ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i); @@ -144,7 +146,7 @@ public final class WidgetsListTableViewHolderBinderTest { widgetItems.add(new WidgetItem( LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo), - mTestProfile, mIconCache, mContext)); + mTestProfile, mIconCache, mContext, widgetManager)); } return widgetItems; } |