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
This commit is contained in:
Rohit Goyal
2024-01-03 23:41:06 +05:30
parent 97eee72e6e
commit 2e6010c421
14 changed files with 174 additions and 12 deletions

View File

@@ -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"
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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 @@ import java.util.UUID;
@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 @@ public class TaplPromiseIconUiTest extends AbstractLauncherUiTest {
}
@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 @@ 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();
}
}

View File

@@ -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+", "")));