diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index 0ec16237879ae..0d6a23bcdc967 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -941,7 +941,8 @@ public final class ShortcutInfo implements Parcelable {
}
/**
- * Sets the intent of a shortcut.
+ * Sets the intent of a shortcut. Alternatively, {@link #setIntents(Intent[])} can be used
+ * to launch an activity with other activities in the back stack.
*
*
This is a mandatory field when publishing a new shortcut with
* {@link ShortcutManager#addDynamicShortcuts(List)} or
@@ -965,7 +966,9 @@ public final class ShortcutInfo implements Parcelable {
}
/**
- * Sets multiple intents instead of a single intent.
+ * Sets multiple intents instead of a single intent, in order to launch an activity with
+ * other activities in back stack. Use {@link TaskStackBuilder} to build intents.
+ * See the {@link ShortcutManager} javadoc for details.
*
* @see Builder#setIntent(Intent)
* @see ShortcutInfo#getIntents()
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index cfd3442cc07b9..81302714d2b67 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -18,6 +18,7 @@ package android.content.pm;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.annotation.UserIdInt;
+import android.app.Activity;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
@@ -30,7 +31,7 @@ import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
/**
- * ShortcutManager manages "launcher shortcuts" (or simply "shortcuts"). Shortcuts provide user
+ * ShortcutManager manages "launcher shortcuts" (or simply "shortcuts"). Shortcuts provide users
* with quick
* ways to access activities other than the main activity from the launcher to users. For example,
* an email application may publish the "compose new email" action which will directly open the
@@ -183,6 +184,7 @@ import java.util.List;
* android:action="android.intent.action.VIEW"
* android:targetPackage="com.example.myapplication"
* android:targetClass="com.example.myapplication.ComposeActivity" />
+ * <!-- more intents can go here; see below -->
* <categories android:name="android.shortcut.conversation" />
* </shortcut>
* <!-- more shortcut can go here -->
@@ -209,9 +211,37 @@ import java.util.List;
*
*
{@code intent} Intent to launch. {@code android:action} is mandatory.
* See Using intents for the
- * other supported tags.
+ * other supported tags. Multiple intents can be provided for a single shortcut, so that
+ * an activity will be launched with other activities in the back stack.
+ * See {@link android.app.TaskStackBuilder} for details.
*
*
+ * Shortcut Intents
+ * Dynamic shortcuts can be published with any {@link Intent#addFlags Intent flags}. Typically,
+ * {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} is specified possibly with other flags; otherwise,
+ * if the application is already running, the application is simply brought to the foreground
+ * and the target activity may not show up.
+ *
+ * {@link ShortcutInfo.Builder#setIntents(Intent[])} can be used (instead of
+ * {@link ShortcutInfo.Builder#setIntent(Intent)}) with
+ * {@link android.app.TaskStackBuilder} in order to launch an activity with other activities
+ * in the back stack, so that when the user presses the back key, a "parent" activity will be shown
+ * instead of the user being navigated back to the launcher.
+ *
+ *
Manifest shortcuts can have multiple intents too to achieve the same effect. In order to
+ * specify multiple {@link Intent}s to a shortcut, simply list multiple <intent>s within
+ * a single <shortcut>. The last intent is what the user will see when a shortcut is
+ * launched.
+ *
+ *
Manifest shortcuts cannot have custom intent flags. The first intent of a manifest
+ * shortcut will always have {@link Intent#FLAG_ACTIVITY_NEW_TASK} and
+ * {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} set. This means, when the application is already
+ * running, all the existing activities will be destroyed when a manifest shortcut is launched.
+ * If this behavior is not desirable, one can use a "trampoline" activity (an activity
+ * that starts another activity in {@link Activity#onCreate} and then calls
+ * {@link Activity#finish()}) with {@code android:taskAffinity=""} in AndroidManifest.xml and point
+ * at this activity in a manifest shortcut's intent.
+ *
*
Updating Shortcuts v.s. Re-publishing New One with Different ID
* In order to avoid users' confusion, {@link #updateShortcuts(List)} should not be used to update
* a shortcut to something that is conceptually different.
@@ -267,6 +297,8 @@ import java.util.List;
* When the user performs "inline reply" on a notification.
*
*
+ * When rate-limiting is active, {@link #isRateLimitingActive()} returns {@code true}.
+ *
*
Resetting rate-limiting for testing
*
* If your application is rate-limited during development or testing, you can use the
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index b4dd5877278d2..698e7622b2fa0 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -459,9 +459,8 @@ public class LauncherAppsService extends SystemService {
}
// Note the target activity doesn't have to be exported.
- // TODO Use sourceBounds
-
- intents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intents[0].setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intents[0].setSourceBounds(sourceBounds);
return startShortcutIntentsAsPublisher(
intents, packageName, startActivityOptions, userId);
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index e6a97394e5b50..185a6a2aadd8d 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -842,12 +842,17 @@ public class ShortcutService extends IShortcutService.Stub {
getLastResetTimeLocked();
}
+ @VisibleForTesting
+ final File getUserFile(@UserIdInt int userId) {
+ return new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
+ }
+
private void saveUserLocked(@UserIdInt int userId) {
- final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
+ final File path = getUserFile(userId);
if (DEBUG) {
Slog.d(TAG, "Saving to " + path);
}
- path.mkdirs();
+ path.getParentFile().mkdirs();
final AtomicFile file = new AtomicFile(path);
FileOutputStream os = null;
try {
@@ -890,7 +895,7 @@ public class ShortcutService extends IShortcutService.Stub {
@Nullable
private ShortcutUser loadUserLocked(@UserIdInt int userId) {
- final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
+ final File path = getUserFile(userId);
if (DEBUG) {
Slog.d(TAG, "Loading from " + path);
}
@@ -1466,7 +1471,7 @@ public class ShortcutService extends IShortcutService.Stub {
* Clean up / validate an incoming shortcut.
* - Make sure all mandatory fields are set.
* - Make sure the intent's extras are persistable, and them to set
- * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras.
+ * {@link ShortcutInfo#mIntentPersistableExtrases}. Also clear its extras.
* - Clear flags.
*
* TODO Detailed unit tests
diff --git a/services/tests/servicestests/assets/shortcut/shortcut_legacy_file.xml b/services/tests/servicestests/assets/shortcut/shortcut_legacy_file.xml
new file mode 100644
index 0000000000000..872dc3a26773d
--- /dev/null
+++ b/services/tests/servicestests/assets/shortcut/shortcut_legacy_file.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index f97355a548372..54b2a15230f78 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -48,9 +48,11 @@ import com.android.frameworks.servicestests.R;
import com.android.server.pm.ShortcutService.ConfigConstants;
import java.io.File;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.io.Writer;
import java.util.Locale;
/**
@@ -98,6 +100,12 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
"action must be set",
() -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(new Intent()));
+ assertExpectException(
+ RuntimeException.class,
+ "action must be set",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id")
+ .setIntents(new Intent[]{new Intent("action"), new Intent()}));
+
assertExpectException(
RuntimeException.class,
"activity cannot be null",
@@ -1967,4 +1975,28 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
mService.dump(null, new PrintWriter(new StringWriter()), null);
});
}
+
+ /**
+ * Make sure the legacy file format that only supported a single intent per shortcut
+ * can still be read.
+ */
+ public void testLoadLegacySavedFile() throws Exception {
+ final File path = mService.getUserFile(USER_0);
+ path.getParentFile().mkdirs();
+ try (Writer w = new FileWriter(path)) {
+ w.write(readTestAsset("shortcut/shortcut_legacy_file.xml"));
+ };
+ initService();
+ mService.handleUnlockUser(USER_0);
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("manifest-shortcut-storage")
+ .forShortcutWithId("manifest-shortcut-storage", si -> {
+ assertEquals("android.settings.INTERNAL_STORAGE_SETTINGS",
+ si.getIntent().getAction());
+ assertEquals(12345, si.getIntent().getIntExtra("key", 0));
+ });
+ });
+ }
}