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
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 228c34a..22ed567 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -97,3 +97,10 @@
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 91da7e6..a57eaa3 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -923,6 +923,8 @@
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 @@
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 190eb78..7cbfc37 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -205,6 +205,8 @@
&& 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 8dc2ab3..7c40a31 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 @@
* - 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 @@
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 @@
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 4f2d398..069e96b 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 @@
* 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 @@
: 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 6c2f589..872ce4b 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 @@
/**
* 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 @@
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 5141db9..58b12b1 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;
@@ -114,6 +116,12 @@
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.
*/
@@ -143,6 +151,15 @@
}
/**
+ * 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
*/
public boolean usingLowResIcon() {
@@ -158,7 +175,7 @@
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 @@
* 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 3ce194d..c67ec5a 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -148,9 +148,19 @@
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 cb3c16c..ca27eb2 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 @@
/**
* Utility class to tracking install sessions
*/
+@SuppressWarnings("NewApi")
public class InstallSessionHelper {
@NonNull
@@ -227,6 +230,11 @@
}
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 41908d3..e4a2045 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 java.lang.ref.WeakReference;
import java.util.Objects;
+@SuppressWarnings("NewApi")
@WorkerThread
public class InstallSessionTracker extends PackageInstaller.SessionCallback {
@@ -77,6 +79,12 @@
}
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 839f98c..ff8b381 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 @@
}
// 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 5cf96c8..2596b75 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -23,6 +23,14 @@
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
+
+ <receiver android:name="com.android.launcher3.compat.PromiseIconUiTest$UnarchiveBroadcastReceiver"
+ android:enabled="true"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.UNARCHIVE_PACKAGE"/>
+ </intent-filter>
+ </receiver>
</application>
<instrumentation
diff --git a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
index 8200c94..2dc1cb2 100644
--- a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
@@ -15,24 +15,38 @@
*/
package com.android.launcher3.compat;
+import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.TextUtils;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.compatibility.common.util.SystemUtil;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
+import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.rule.ViewCaptureRule;
import org.junit.After;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
import java.util.UUID;
@@ -43,6 +57,14 @@
@RunWith(AndroidJUnit4.class)
public class TaplPromiseIconUiTest extends AbstractLauncherUiTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ public static final String PACKAGE_NAME = "test.promise.app";
+ public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
+ public static final String DUMMY_LABEL = "Aardwolf";
+
private int mSessionId = -1;
@Override
@@ -55,18 +77,19 @@
}
@After
- public void tearDown() {
+ public void tearDown() throws IOException {
if (mSessionId > -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 @@
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 @@
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 @@
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 683f323..95444ba 100644
--- a/tests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/src/com/android/launcher3/util/TestUtil.java
@@ -103,7 +103,9 @@
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+", "")));