Merge "When app is updated, save the new version code, and update shortcuts with resource based icons." into nyc-dev
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 8e96a58..d7f8cc6 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 @@
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 @@
*/
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 @@
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 @@
getPackageInfo().dump(s, pw, prefix + " ");
pw.println();
- pw.println(" Shortcuts:");
+ pw.print(prefix);
+ pw.println(" Shortcuts:");
long totalBitmapSize = 0;
final ArrayMap<String, ShortcutInfo> 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 2c45890..74969f0 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 @@
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<byte[]> mSigHashes;
private ShortcutPackageInfo(int versionCode, ArrayList<byte[]> sigHashes, boolean isShadow) {
@@ -60,7 +62,7 @@
}
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 @@
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 0ac5c1f..5764161 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 @@
// Preload
getUserShortcutsLocked(userId);
- cleanupGonePackages(userId);
+ checkPackageChanges(userId);
}
}
@@ -1158,7 +1158,11 @@
* - 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 @@
ps.addDynamicShortcut(this, newShortcut);
}
}
- userPackageChanged(packageName, userId);
+ packageShortcutsChanged(packageName, userId);
return true;
}
@@ -1323,7 +1327,7 @@
}
}
}
- userPackageChanged(packageName, userId);
+ packageShortcutsChanged(packageName, userId);
return true;
}
@@ -1353,7 +1357,7 @@
ps.addDynamicShortcut(this, newShortcut);
}
}
- userPackageChanged(packageName, userId);
+ packageShortcutsChanged(packageName, userId);
return true;
}
@@ -1370,7 +1374,7 @@
Preconditions.checkStringNotEmpty((String) shortcutIds.get(i)));
}
}
- userPackageChanged(packageName, userId);
+ packageShortcutsChanged(packageName, userId);
}
@Override
@@ -1380,7 +1384,7 @@
synchronized (mLock) {
getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
}
- userPackageChanged(packageName, userId);
+ packageShortcutsChanged(packageName, userId);
}
@Override
@@ -1729,7 +1733,7 @@
launcher.pinShortcuts(
ShortcutService.this, userId, packageName, shortcutIds);
}
- userPackageChanged(packageName, userId);
+ packageShortcutsChanged(packageName, userId);
}
@Override
@@ -1841,13 +1845,15 @@
};
/**
- * 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<PackageWithUser> gonePackages = new ArrayList<>();
@@ -1858,10 +1864,15 @@
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 @@
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 @@
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 @@
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 0b8c3a2..3d2e2ec 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 @@
});
}
+ /**
+ * 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 5387f31..bc43576 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.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 @@
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 @@
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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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<List> 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 d09b62c..ad49c2f 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 static ShortcutInfo findShortcut(List<ShortcutInfo> 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 {