From 2e6010c421720db22f08361c96ae7503342c280d Mon Sep 17 00:00:00 2001 From: Rohit Goyal Date: Wed, 3 Jan 2024 23:41:06 +0530 Subject: [PATCH] Add support for progress bar during unarchival of app. * With this change, progress bar should appear whenever app update for archived app reaches non-zero progress. * Once the update is cancelled, the progress bar should also disappear. Test: PromiseIconUiTest and manually tested the progress bar using Play Store Bug: 302115555 Flag: ACONFIG com.android.launcher3.enable_support_for_archiving DEVELOPMENT Change-Id: Iacf7a0fd865dba34915fa09f59c63a1da6e47315 --- aconfig/launcher.aconfig | 7 ++ src/com/android/launcher3/BubbleTextView.java | 3 + .../android/launcher3/model/AllAppsList.java | 2 + .../android/launcher3/model/LoaderTask.java | 16 ++++- .../launcher3/model/PackageUpdatedTask.java | 13 ++++ .../android/launcher3/model/data/AppInfo.java | 5 ++ .../model/data/ItemInfoWithIcon.java | 24 ++++++- .../model/data/WorkspaceItemInfo.java | 12 +++- .../launcher3/pm/InstallSessionHelper.java | 8 +++ .../launcher3/pm/InstallSessionTracker.java | 8 +++ .../launcher3/touch/ItemClickHandler.java | 4 +- tests/AndroidManifest.xml | 8 +++ .../compat/TaplPromiseIconUiTest.java | 72 +++++++++++++++++-- .../com/android/launcher3/util/TestUtil.java | 4 +- 14 files changed, 174 insertions(+), 12 deletions(-) diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index 228c34a8e5..22ed567ec4 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -97,3 +97,10 @@ flag { description: "Enables full width two pane widget picker for tablets in landscape and portrait" bug: "315055849" } + +flag { + name: "enable_support_for_archiving" + namespace: "launcher" + description: "Enables support for archived apps in Launcher3, such as empty progress bar etc." + bug: "210590852" +} diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 91da7e6404..a57eaa3ca6 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -923,6 +923,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, if (mIcon instanceof PreloadIconDrawable) { preloadIconDrawable = (PreloadIconDrawable) mIcon; preloadIconDrawable.setLevel(progressLevel); + // TODO(b/302115555): For archived apps, show icon as disabled if active session + // exists. preloadIconDrawable.setIsDisabled(info.getProgressLevel() == 0); } else { preloadIconDrawable = makePreloadIcon(); @@ -948,6 +950,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info); preloadDrawable.setLevel(progressLevel); + // TODO(b/302115555): For archived apps, show icon as disabled if active session exists. preloadDrawable.setIsDisabled(info.getProgressLevel() == 0); return preloadDrawable; } diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java index 190eb78d2a..7cbfc37f07 100644 --- a/src/com/android/launcher3/model/AllAppsList.java +++ b/src/com/android/launcher3/model/AllAppsList.java @@ -205,6 +205,8 @@ public class AllAppsList { && installInfo.state == PackageInstallInfo.STATUS_INSTALLING) { continue; } + // TODO(b/302115555): Handle the case when archived apps are to be updated + // during unarchival start. appInfo.setProgressLevel(installInfo); updatedAppInfos.add(appInfo); diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 8dc2ab3175..7c40a31e62 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -17,6 +17,7 @@ package com.android.launcher3.model; import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN; +import static com.android.launcher3.Flags.enableSupportForArchiving; import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; @@ -131,6 +132,7 @@ import java.util.concurrent.CancellationException; * - all apps icons * - deep shortcuts within apps */ +@SuppressWarnings("NewApi") public class LoaderTask implements Runnable { private static final String TAG = "LoaderTask"; public static final String SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen"; @@ -772,13 +774,21 @@ public class LoaderTask implements Runnable { PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); } - if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) { + if ((c.restoreFlag != 0 + || (enableSupportForArchiving() + && activityInfo != null + && activityInfo.getApplicationInfo().isArchived)) + && !TextUtils.isEmpty(targetPkg)) { tempPackageKey.update(targetPkg, c.user); SessionInfo si = installingPkgs.get(tempPackageKey); if (si == null) { info.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; - } else if (activityInfo == null) { + } else if (activityInfo == null + // For archived apps, include progress info in case there is + // a pending install session post restart of device. + || (enableSupportForArchiving() + && activityInfo.getApplicationInfo().isArchived)) { int installProgress = (int) (si.getProgress() * 100); info.setProgressLevel(installProgress, @@ -1095,6 +1105,8 @@ public class LoaderTask implements Runnable { for (int i = 0; i < apps.size(); i++) { LauncherActivityInfo app = apps.get(i); AppInfo appInfo = new AppInfo(app, user, quietMode); + // TODO(b/302115555): Handle the case when archived apps with active sessions are + // loaded. iconRequestInfos.add(new IconRequestInfo<>( appInfo, app, /* useLowResIcon= */ false)); diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 4f2d398b42..069e96b456 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -15,9 +15,11 @@ */ package com.android.launcher3.model; +import static com.android.launcher3.Flags.enableSupportForArchiving; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON; @@ -67,6 +69,7 @@ import java.util.stream.Collectors; * Handles updates due to changes in package manager (app installed/updated/removed) * or when a user availability changes. */ +@SuppressWarnings("NewApi") public class PackageUpdatedTask extends BaseModelUpdateTask { // TODO(b/290090023): Set to false after root causing is done. @@ -269,6 +272,16 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { : PackageManagerHelper.getLoadingProgress( activities.get(0)), PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); + // In case an app is archived, we need to make sure that archived state + // in WorkspaceItemInfo is refreshed. + if (enableSupportForArchiving() && !activities.isEmpty()) { + boolean newArchivalState = activities.get( + 0).getActivityInfo().isArchived; + if (newArchivalState != si.isArchived()) { + si.runtimeStatusFlags ^= FLAG_ARCHIVED; + infoUpdated = true; + } + } if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) { iconCache.getTitleAndIcon(si, si.usingLowResIcon()); infoUpdated = true; diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java index 6c2f5890be..872ce4bdba 100644 --- a/src/com/android/launcher3/model/data/AppInfo.java +++ b/src/com/android/launcher3/model/data/AppInfo.java @@ -16,6 +16,7 @@ package com.android.launcher3.model.data; +import static com.android.launcher3.Flags.enableSupportForArchiving; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS; import android.content.ComponentName; @@ -40,6 +41,7 @@ import java.util.Comparator; /** * Represents an app in AllAppsView. */ +@SuppressWarnings("NewApi") public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { public static final AppInfo[] EMPTY_ARRAY = new AppInfo[0]; @@ -172,6 +174,9 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { if (PackageManagerHelper.isAppSuspended(appInfo)) { info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED; } + if (enableSupportForArchiving() && lai.getActivityInfo().isArchived) { + info.runtimeStatusFlags |= FLAG_ARCHIVED; + } info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES; diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java index 5141db9a24..58b12b17d8 100644 --- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java @@ -16,6 +16,8 @@ package com.android.launcher3.model.data; +import static com.android.launcher3.Flags.enableSupportForArchiving; + import android.content.Context; import android.content.Intent; import android.os.Process; @@ -113,6 +115,12 @@ public abstract class ItemInfoWithIcon extends ItemInfo { */ public static final int FLAG_NOT_PINNABLE = 1 << 13; + /** + * Flag indicating whether the package related to the item & user corresponds to that of + * archived app. + */ + public static final int FLAG_ARCHIVED = 1 << 14; + /** * 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. @@ -142,6 +150,15 @@ public abstract class ItemInfoWithIcon extends ItemInfo { return (runtimeStatusFlags & FLAG_DISABLED_MASK) != 0; } + /** + * Returns true if the app corresponding to the item is archived. */ + public boolean isArchived() { + if (!enableSupportForArchiving()) { + return false; + } + return (runtimeStatusFlags & FLAG_ARCHIVED) != 0; + } + /** * Indicates whether we're using a low res icon */ @@ -158,7 +175,7 @@ public abstract class ItemInfoWithIcon extends ItemInfo { public boolean isAppStartable() { return ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) == 0) && (((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) - || mProgressLevel == 100); + || mProgressLevel == 100 || isArchived()); } /** @@ -167,7 +184,10 @@ public abstract class ItemInfoWithIcon extends ItemInfo { * progress. */ public int getProgressLevel() { - if ((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { + if (((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) + // This condition for archived apps is so that in case unarchival/update of + // archived app is cancelled, the state transitions back to 0% installed state. + || isArchived()) { return mProgressLevel; } return 100; diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java index 3ce194dd81..c67ec5a40d 100644 --- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java +++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java @@ -148,9 +148,19 @@ public class WorkspaceItemInfo extends ItemInfoWithIcon { public final boolean isPromise() { - return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON); + return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON) + // For archived apps, promise icons are always ready to be displayed. + || isArchived(); } + /** + * Returns true if the workspace item supports promise icon UI. There are a few cases where they + * are supported: + * 1. Icons to be restored via backup/restore. + * 2. Icons added as an auto-install app. + * 3. Icons added due to it being an active install session created by the user. + * 4. Icons for archived apps. + */ public boolean hasPromiseIconUi() { return isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI); } diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java index cb3c16c8aa..ca27eb28c4 100644 --- a/src/com/android/launcher3/pm/InstallSessionHelper.java +++ b/src/com/android/launcher3/pm/InstallSessionHelper.java @@ -16,6 +16,8 @@ package com.android.launcher3.pm; +import static com.android.launcher3.Flags.enableSupportForArchiving; + import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; @@ -51,6 +53,7 @@ import java.util.Objects; /** * Utility class to tracking install sessions */ +@SuppressWarnings("NewApi") public class InstallSessionHelper { @NonNull @@ -227,6 +230,11 @@ public class InstallSessionHelper { } public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) { + // For archived apps we always want to show promise icons and the checks below don't apply. + if (enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) { + return true; + } + return verify(sessionInfo) != null && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER && sessionInfo.getAppIcon() != null diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java index 41908d3496..e4a2045af3 100644 --- a/src/com/android/launcher3/pm/InstallSessionTracker.java +++ b/src/com/android/launcher3/pm/InstallSessionTracker.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.pm; +import static com.android.launcher3.Flags.enableSupportForArchiving; import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle; import static com.android.launcher3.pm.PackageInstallInfo.STATUS_FAILED; import static com.android.launcher3.pm.PackageInstallInfo.STATUS_INSTALLED; @@ -36,6 +37,7 @@ import com.android.launcher3.util.PackageUserKey; import java.lang.ref.WeakReference; import java.util.Objects; +@SuppressWarnings("NewApi") @WorkerThread public class InstallSessionTracker extends PackageInstaller.SessionCallback { @@ -77,6 +79,12 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { } helper.tryQueuePromiseAppIcon(sessionInfo); + + if (enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) { + // For archived apps, icon could already be present on the workspace. To make sure + // the icon state is updated, we send a change event. + callback.onPackageStateChanged(PackageInstallInfo.fromInstallingState(sessionInfo)); + } } @Override diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index 839f98c3fc..ff8b381779 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.touch; +import static com.android.launcher3.Flags.enableSupportForArchiving; import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET; import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; @@ -319,7 +320,8 @@ public class ItemClickHandler { } // Check for abandoned promise - if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) { + if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi() + && (!enableSupportForArchiving() || !shortcut.isArchived())) { String packageName = shortcut.getIntent().getComponent() != null ? shortcut.getIntent().getComponent().getPackageName() : shortcut.getIntent().getPackage(); diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 5cf96c8554..2596b75fb5 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -23,6 +23,14 @@ + + + + + + -1) { mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); } + TestUtil.uninstallDummyApp(); } /** * Create a session and return the id. */ - private int createSession(String label, Bitmap icon) throws Throwable { + private int createSession(String packageName, String label, Bitmap icon) throws Throwable { SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL); - params.setAppPackageName("test.promise.app"); + params.setAppPackageName(packageName); params.setAppLabel(label); params.setAppIcon(icon); params.setInstallReason(PackageManager.INSTALL_REASON_USER); @@ -80,7 +103,8 @@ public class TaplPromiseIconUiTest extends AbstractLauncherUiTest { info != null && TextUtils.equals(info.title, appLabel); // Create and add test session - mSessionId = createSession(appLabel, Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)); + mSessionId = createSession(PACKAGE_NAME, appLabel, + Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)); // Verify promise icon is added waitForLauncherCondition("Test Promise App not found on workspace", launcher -> @@ -103,7 +127,7 @@ public class TaplPromiseIconUiTest extends AbstractLauncherUiTest { info != null && TextUtils.equals(info.title, appLabel); // Create and add test session without icon or label - mSessionId = createSession(null, null); + mSessionId = createSession(PACKAGE_NAME, null, null); // Sleep for duration of animation if a view was to be added + some buffer time. Thread.sleep(Launcher.NEW_APPS_PAGE_MOVE_DELAY + Launcher.NEW_APPS_ANIMATION_DELAY + 500); @@ -112,4 +136,42 @@ public class TaplPromiseIconUiTest extends AbstractLauncherUiTest { waitForLauncherCondition("Test Promise App not found on workspace", launcher -> launcher.getWorkspace().getFirstMatch(findPromiseApp) == null); } + + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING) + public void testPromiseIcon_addedArchivedApp() throws Throwable { + installDummyAppAndWaitForUIUpdate(); + assertThat( + SystemUtil.runShellCommand( + String.format("pm archive %s", DUMMY_PACKAGE))).isEqualTo( + "Success\n"); + + final ItemOperator findPromiseApp = (info, view) -> + info != null && TextUtils.equals(info.title, DUMMY_LABEL); + + // Create and add test session + mSessionId = createSession(DUMMY_PACKAGE, /* label= */ "", + Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)); + + // Verify promise icon is added + waitForLauncherCondition("Test Promise App not found on workspace", launcher -> + launcher.getWorkspace().getFirstMatch(findPromiseApp) != null); + + // Remove session + mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); + mSessionId = -1; + } + + // Dummy receiver to fulfill archiving platform requirements, unused in reality. + public static class UnarchiveBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + } + } + + private void installDummyAppAndWaitForUIUpdate() throws IOException { + TestUtil.installDummyApp(); + mLauncher.waitForModelQueueCleared(); + mLauncher.waitForLauncherInitialized(); + } } diff --git a/tests/src/com/android/launcher3/util/TestUtil.java b/tests/src/com/android/launcher3/util/TestUtil.java index 683f3238a0..95444ba3f2 100644 --- a/tests/src/com/android/launcher3/util/TestUtil.java +++ b/tests/src/com/android/launcher3/util/TestUtil.java @@ -103,7 +103,9 @@ public class TestUtil { out.close(); final String result = UiDevice.getInstance(instrumentation) - .executeShellCommand("pm install --user " + userId + " " + apkFilename); + .executeShellCommand(String.format("pm install -i %s --user ", + instrumentation.getContext().getPackageName()) + + userId + " " + apkFilename); Assert.assertTrue( "Failed to install wellbeing test apk; make sure the device is rooted", "Success".equals(result.replaceAll("\\s+", "")));