diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 8e96a580b0bbe..d7f8cc67a5f06 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -109,6 +109,27 @@ class ShortcutPackage extends ShortcutPackageItem { return getPackageUserId(); } + /** + * Called when a shortcut is about to be published. At this point we know the publisher package + * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so + * we do some initialization for the package. + */ + private void onShortcutPublish(ShortcutService s) { + // Make sure we have the version code for the app. We need the version code in + // handlePackageUpdated(). + if (getPackageInfo().getVersionCode() < 0) { + final int versionCode = s.getApplicationVersionCode(getPackageName(), getOwnerUserId()); + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format("Package %s version = %d", getPackageName(), + versionCode)); + } + if (versionCode >= 0) { + getPackageInfo().setVersionCode(versionCode); + s.scheduleSaveUser(getOwnerUserId()); + } + } + } + @Override protected void onRestoreBlocked(ShortcutService s) { // Can't restore due to version/signature mismatch. Remove all shortcuts. @@ -153,6 +174,9 @@ class ShortcutPackage extends ShortcutPackageItem { */ public void addDynamicShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) { + + onShortcutPublish(s); + newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); @@ -387,6 +411,40 @@ class ShortcutPackage extends ShortcutPackageItem { mApiCallCount = 0; } + /** + * Called when the package is updated. If there are shortcuts with resource icons, update + * their timestamps. + */ + public void handlePackageUpdated(ShortcutService s, int newVersionCode) { + if (getPackageInfo().getVersionCode() >= newVersionCode) { + // Version hasn't changed; nothing to do. + return; + } + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format("Package %s updated, version %d -> %d", getPackageName(), + getPackageInfo().getVersionCode(), newVersionCode)); + } + + getPackageInfo().setVersionCode(newVersionCode); + + boolean changed = false; + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + + if (si.hasIconResource()) { + changed = true; + si.setTimestamp(s.injectCurrentTimeMillis()); + } + } + if (changed) { + // This will send a notification to the launcher, and also save . + s.packageShortcutsChanged(getPackageName(), getPackageUserId()); + } else { + // Still save the version code. + s.scheduleSaveUser(getPackageUserId()); + } + } + public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) { pw.println(); @@ -413,17 +471,20 @@ class ShortcutPackage extends ShortcutPackageItem { getPackageInfo().dump(s, pw, prefix + " "); pw.println(); - pw.println(" Shortcuts:"); + pw.print(prefix); + pw.println(" Shortcuts:"); long totalBitmapSize = 0; final ArrayMap shortcuts = mShortcuts; final int size = shortcuts.size(); for (int i = 0; i < size; i++) { final ShortcutInfo si = shortcuts.valueAt(i); - pw.print(" "); + pw.print(prefix); + pw.print(" "); pw.println(si.toInsecureString()); if (si.getBitmapPath() != null) { final long len = new File(si.getBitmapPath()).length(); - pw.print(" "); + pw.print(prefix); + pw.print(" "); pw.print("bitmap size="); pw.println(len); diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java index 2c45890a9c408..74969f02d7026 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java +++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java @@ -45,12 +45,14 @@ class ShortcutPackageInfo { private static final String TAG_SIGNATURE = "signature"; private static final String ATTR_SIGNATURE_HASH = "hash"; + private static final int VERSION_UNKNOWN = -1; + /** * When true, this package information was restored from the previous device, and the app hasn't * been installed yet. */ private boolean mIsShadow; - private int mVersionCode; + private int mVersionCode = VERSION_UNKNOWN; private ArrayList mSigHashes; private ShortcutPackageInfo(int versionCode, ArrayList sigHashes, boolean isShadow) { @@ -60,7 +62,7 @@ class ShortcutPackageInfo { } public static ShortcutPackageInfo newEmpty() { - return new ShortcutPackageInfo(0, new ArrayList<>(0), /* isShadow */ false); + return new ShortcutPackageInfo(VERSION_UNKNOWN, new ArrayList<>(0), /* isShadow */ false); } public boolean isShadow() { @@ -75,6 +77,10 @@ class ShortcutPackageInfo { return mVersionCode; } + public void setVersionCode(int versionCode) { + mVersionCode = versionCode; + } + public boolean hasSignatures() { return mSigHashes.size() > 0; } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 0ac5c1fef30c3..57641614cda84 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -356,7 +356,7 @@ public class ShortcutService extends IShortcutService.Stub { // Preload getUserShortcutsLocked(userId); - cleanupGonePackages(userId); + checkPackageChanges(userId); } } @@ -1158,7 +1158,11 @@ public class ShortcutService extends IShortcutService.Stub { * - Sends a notification to LauncherApps * - Write to file */ - private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { + void packageShortcutsChanged(@NonNull String packageName, @UserIdInt int userId) { + if (DEBUG) { + Slog.d(TAG, String.format( + "Shortcut changes: package=%s, user=%d", packageName, userId)); + } notifyListeners(packageName, userId); scheduleSaveUser(userId); } @@ -1284,7 +1288,7 @@ public class ShortcutService extends IShortcutService.Stub { ps.addDynamicShortcut(this, newShortcut); } } - userPackageChanged(packageName, userId); + packageShortcutsChanged(packageName, userId); return true; } @@ -1323,7 +1327,7 @@ public class ShortcutService extends IShortcutService.Stub { } } } - userPackageChanged(packageName, userId); + packageShortcutsChanged(packageName, userId); return true; } @@ -1353,7 +1357,7 @@ public class ShortcutService extends IShortcutService.Stub { ps.addDynamicShortcut(this, newShortcut); } } - userPackageChanged(packageName, userId); + packageShortcutsChanged(packageName, userId); return true; } @@ -1370,7 +1374,7 @@ public class ShortcutService extends IShortcutService.Stub { Preconditions.checkStringNotEmpty((String) shortcutIds.get(i))); } } - userPackageChanged(packageName, userId); + packageShortcutsChanged(packageName, userId); } @Override @@ -1380,7 +1384,7 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this); } - userPackageChanged(packageName, userId); + packageShortcutsChanged(packageName, userId); } @Override @@ -1729,7 +1733,7 @@ public class ShortcutService extends IShortcutService.Stub { launcher.pinShortcuts( ShortcutService.this, userId, packageName, shortcutIds); } - userPackageChanged(packageName, userId); + packageShortcutsChanged(packageName, userId); } @Override @@ -1841,13 +1845,15 @@ public class ShortcutService extends IShortcutService.Stub { }; /** - * Called when a user is unlocked. Check all known packages still exist, and otherwise - * perform cleanup. + * Called when a user is unlocked. + * - Check all known packages still exist, and otherwise perform cleanup. + * - If a package still exists, check the version code. If it's been updated, may need to + * update timestamps of its shortcuts. */ @VisibleForTesting - void cleanupGonePackages(@UserIdInt int ownerUserId) { + void checkPackageChanges(@UserIdInt int ownerUserId) { if (DEBUG) { - Slog.d(TAG, "cleanupGonePackages() ownerUserId=" + ownerUserId); + Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId); } final ArrayList gonePackages = new ArrayList<>(); @@ -1858,10 +1864,15 @@ public class ShortcutService extends IShortcutService.Stub { if (spi.getPackageInfo().isShadow()) { return; // Don't delete shadow information. } - if (isPackageInstalled(spi.getPackageName(), spi.getPackageUserId())) { - return; // Package not gone. + final int versionCode = getApplicationVersionCode( + spi.getPackageName(), spi.getPackageUserId()); + if (versionCode >= 0) { + // Package still installed, see if it's updated. + getUserShortcutsLocked(ownerUserId).handlePackageUpdated( + this, spi.getPackageName(), versionCode); + } else { + gonePackages.add(PackageWithUser.of(spi)); } - gonePackages.add(PackageWithUser.of(spi)); }); if (gonePackages.size() > 0) { for (int i = gonePackages.size() - 1; i >= 0; i--) { @@ -1890,6 +1901,12 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { forEachLoadedUserLocked(user -> user.attemptToRestoreIfNeededAndSave(this, packageName, userId)); + + final int versionCode = getApplicationVersionCode(packageName, userId); + if (versionCode < 0) { + return; // shouldn't happen + } + getUserShortcutsLocked(userId).handlePackageUpdated(this, packageName, versionCode); } } @@ -1978,6 +1995,17 @@ public class ShortcutService extends IShortcutService.Stub { return isApplicationFlagSet(packageName, userId, ApplicationInfo.FLAG_INSTALLED); } + /** + * @return the version code of the package, or -1 if the app is not installed. + */ + int getApplicationVersionCode(String packageName, int userId) { + final ApplicationInfo ai = injectApplicationInfo(packageName, userId); + if ((ai == null) || ((ai.flags & ApplicationInfo.FLAG_INSTALLED) == 0)) { + return -1; + } + return ai.versionCode; + } + // === Backup & restore === boolean shouldBackupApp(String packageName, int userId) { @@ -2100,7 +2128,7 @@ public class ShortcutService extends IShortcutService.Stub { pw.println(mResetInterval); pw.print(" maxUpdatesPerInterval: "); pw.println(mMaxUpdatesPerInterval); - pw.print(" maxDynamicShortcuts:"); + pw.print(" maxDynamicShortcuts: "); pw.println(mMaxDynamicShortcuts); pw.println(); diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java index 0b8c3a2ee8beb..3d2e2ec491388 100644 --- a/services/core/java/com/android/server/pm/ShortcutUser.java +++ b/services/core/java/com/android/server/pm/ShortcutUser.java @@ -176,6 +176,17 @@ class ShortcutUser { }); } + /** + * Called when a package is updated. + */ + public void handlePackageUpdated(ShortcutService s, @NonNull String packageName, + int newVersionCode) { + if (!mPackages.containsKey(packageName)) { + return; + } + getPackageShortcuts(s, packageName).handlePackageUpdated(s, newVersionCode); + } + public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName, @UserIdInt int packageUserId) { forPackageItem(packageName, packageUserId, spi -> { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java index 5387f312a10df..bc43576c7caf5 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java @@ -36,6 +36,7 @@ import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicOnly; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.findShortcut; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.hashSet; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.makeBundle; @@ -644,6 +645,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pi.applicationInfo.flags = ApplicationInfo.FLAG_INSTALLED | ApplicationInfo.FLAG_ALLOW_BACKUP; pi.versionCode = version; + pi.applicationInfo.versionCode = version; pi.signatures = genSignatures(signatures); return pi; @@ -657,6 +659,13 @@ public class ShortcutManagerTest extends InstrumentationTestCase { c.accept(mInjectedPackages.get(packageName)); } + private void updatePackageVersion(String packageName, int increment) { + updatePackageInfo(packageName, pi -> { + pi.versionCode += increment; + pi.applicationInfo.versionCode += increment; + }); + } + private void uninstallPackage(int userId, String packageName) { if (ENABLE_DUMP) { Log.i(TAG, "Unnstall package " + packageName + " / " + userId); @@ -3839,7 +3848,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { // Start uninstalling. uninstallPackage(USER_10, LAUNCHER_1); - mService.cleanupGonePackages(USER_10); + mService.checkPackageChanges(USER_10); assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0)); assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0)); @@ -3859,7 +3868,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { // Uninstall. uninstallPackage(USER_10, CALLING_PACKAGE_1); - mService.cleanupGonePackages(USER_10); + mService.checkPackageChanges(USER_10); assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0)); assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0)); @@ -3878,7 +3887,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10)); uninstallPackage(USER_P0, LAUNCHER_1); - mService.cleanupGonePackages(USER_0); + mService.checkPackageChanges(USER_0); assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0)); assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0)); @@ -3896,7 +3905,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10)); assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10)); - mService.cleanupGonePackages(USER_P0); + mService.checkPackageChanges(USER_P0); assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0)); assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0)); @@ -4150,6 +4159,193 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10)); } + public void testHandlePackageUpdate() throws Throwable { + + // Set up shortcuts and launchers. + + final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); + final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource( + getTestContext().getResources(), R.drawable.black_32x32)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcutWithIcon("s2", res32x32), + makeShortcutWithIcon("s3", res32x32), + makeShortcutWithIcon("s4", bmp32x32)))); + }); + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcutWithIcon("s2", bmp32x32)))); + }); + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list( + makeShortcutWithIcon("s1", res32x32)))); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { + assertTrue(mManager.setDynamicShortcuts(list( + makeShortcutWithIcon("s1", res32x32), + makeShortcutWithIcon("s2", res32x32)))); + }); + runWithCaller(CALLING_PACKAGE_2, USER_10, () -> { + assertTrue(mManager.setDynamicShortcuts(list( + makeShortcutWithIcon("s1", bmp32x32), + makeShortcutWithIcon("s2", bmp32x32)))); + }); + + LauncherApps.Callback c0 = mock(LauncherApps.Callback.class); + LauncherApps.Callback c10 = mock(LauncherApps.Callback.class); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper())); + }); + runWithCaller(LAUNCHER_1, USER_10, () -> { + mLauncherApps.registerCallback(c10, new Handler(Looper.getMainLooper())); + }); + + mInjectedCurrentTimeLillis = START_TIME + 100; + + ArgumentCaptor shortcuts; + + // First, call the event without updating the versions. + reset(c0); + reset(c10); + + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0)); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageUpdateIntent(CALLING_PACKAGE_1, USER_10)); + + waitOnMainThread(); + + // Version not changed, so no callback. + verify(c0, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + any(List.class), + any(UserHandle.class)); + verify(c10, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + any(List.class), + any(UserHandle.class)); + + // Next, update the version info for package 1. + reset(c0); + reset(c10); + updatePackageVersion(CALLING_PACKAGE_1, 1); + + // Then send the broadcast, to only user-0. + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0)); + + waitOnMainThread(); + + // User-0 should get the notification. + shortcuts = ArgumentCaptor.forClass(List.class); + verify(c0).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + shortcuts.capture(), + eq(HANDLE_USER_0)); + + // User-10 shouldn't yet get the notification. + verify(c10, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + any(List.class), + any(UserHandle.class)); + assertShortcutIds(shortcuts.getValue(), "s1", "s2", "s3", "s4"); + assertEquals(START_TIME, + findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp()); + assertEquals(START_TIME + 100, + findShortcut(shortcuts.getValue(), "s2").getLastChangedTimestamp()); + assertEquals(START_TIME + 100, + findShortcut(shortcuts.getValue(), "s3").getLastChangedTimestamp()); + assertEquals(START_TIME, + findShortcut(shortcuts.getValue(), "s4").getLastChangedTimestamp()); + + // Next, send unlock even on user-10. Now we scan packages on this user and send a + // notification to the launcher. + mInjectedCurrentTimeLillis = START_TIME + 200; + + when(mMockUserManager.isUserRunning(eq(USER_10))).thenReturn(true); + + reset(c0); + reset(c10); + mService.handleUnlockUser(USER_10); + + shortcuts = ArgumentCaptor.forClass(List.class); + verify(c0, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + any(List.class), + any(UserHandle.class)); + + verify(c10).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + shortcuts.capture(), + eq(HANDLE_USER_10)); + + assertShortcutIds(shortcuts.getValue(), "s1", "s2"); + assertEquals(START_TIME + 200, + findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp()); + assertEquals(START_TIME + 200, + findShortcut(shortcuts.getValue(), "s2").getLastChangedTimestamp()); + + + // Do the same thing for package 2, which doesn't have resource icons. + mInjectedCurrentTimeLillis = START_TIME + 300; + + reset(c0); + reset(c10); + updatePackageVersion(CALLING_PACKAGE_2, 10); + + // Then send the broadcast, to only user-0. + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageUpdateIntent(CALLING_PACKAGE_2, USER_0)); + mService.handleUnlockUser(USER_10); + + waitOnMainThread(); + + verify(c0, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + any(List.class), + any(UserHandle.class)); + + verify(c10, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_1), + any(List.class), + any(UserHandle.class)); + + // Do the same thing for package 3 + mInjectedCurrentTimeLillis = START_TIME + 400; + + reset(c0); + reset(c10); + updatePackageVersion(CALLING_PACKAGE_3, 100); + + // Then send the broadcast, to only user-0. + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageUpdateIntent(CALLING_PACKAGE_3, USER_0)); + mService.handleUnlockUser(USER_10); + + waitOnMainThread(); + + shortcuts = ArgumentCaptor.forClass(List.class); + verify(c0).onShortcutsChanged( + eq(CALLING_PACKAGE_3), + shortcuts.capture(), + eq(HANDLE_USER_0)); + + // User 10 doesn't have package 3, so no callback. + verify(c10, times(0)).onShortcutsChanged( + eq(CALLING_PACKAGE_3), + any(List.class), + any(UserHandle.class)); + + assertShortcutIds(shortcuts.getValue(), "s1"); + assertEquals(START_TIME + 400, + findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp()); + } + private void backupAndRestore() { int prevUid = mInjectedCallingUid; diff --git a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java index d09b62c2dec65..ad49c2ff78e8f 100644 --- a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java +++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java @@ -413,6 +413,16 @@ public class ShortcutManagerTestUtils { } } + public static ShortcutInfo findShortcut(List list, String id) { + for (ShortcutInfo si : list) { + if (si.getId().equals(id)) { + return si; + } + } + fail("Shortcut " + id + " not found in the list"); + return null; + } + public static Bitmap pfdToBitmap(ParcelFileDescriptor pfd) { assertNotNull(pfd); try {