Adds ShortcutManager#pushDynamicShortcut(ShortcutInfo)
Bug: 137397357
Bug: 148561453
Test: atest com.android.server.pm.ShortcutManagerTest1 \
com.android.server.pm.ShortcutManagerTest2 \
com.android.server.pm.ShortcutManagerTest3 \
com.android.server.pm.ShortcutManagerTest4 \
com.android.server.pm.ShortcutManagerTest5 \
com.android.server.pm.ShortcutManagerTest6 \
com.android.server.pm.ShortcutManagerTest7 \
com.android.server.pm.ShortcutManagerTest8 \
com.android.server.pm.ShortcutManagerTest9 \
com.android.server.pm.ShortcutManagerTest10
Change-Id: I20585a978bfee961df3326f38b11766ea403a5cd
This commit is contained in:
@@ -12457,6 +12457,7 @@ package android.content.pm {
|
||||
method @NonNull public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(int);
|
||||
method public boolean isRateLimitingActive();
|
||||
method public boolean isRequestPinShortcutSupported();
|
||||
method public void pushDynamicShortcut(@NonNull android.content.pm.ShortcutInfo);
|
||||
method public void removeAllDynamicShortcuts();
|
||||
method public void removeDynamicShortcuts(@NonNull java.util.List<java.lang.String>);
|
||||
method public void removeLongLivedShortcuts(@NonNull java.util.List<java.lang.String>);
|
||||
|
||||
@@ -76,4 +76,6 @@ interface IShortcutService {
|
||||
void removeLongLivedShortcuts(String packageName, in List shortcutIds, int userId);
|
||||
|
||||
ParceledListSlice getShortcuts(String packageName, int matchFlags, int userId);
|
||||
|
||||
void pushDynamicShortcut(String packageName, in ShortcutInfo shortcut, int userId);
|
||||
}
|
||||
@@ -1726,11 +1726,11 @@ public final class ShortcutInfo implements Parcelable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if pinned but neither static nor dynamic.
|
||||
* @return true if pinned or cached, but neither static nor dynamic.
|
||||
* @hide
|
||||
*/
|
||||
public boolean isFloating() {
|
||||
return isPinned() && !(isDynamic() || isManifestShortcut());
|
||||
return (isPinned() || isCached()) && !(isDynamic() || isManifestShortcut());
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.annotation.SystemApi;
|
||||
import android.annotation.SystemService;
|
||||
import android.annotation.TestApi;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.Notification;
|
||||
import android.app.usage.UsageStatsManager;
|
||||
import android.compat.annotation.UnsupportedAppUsage;
|
||||
import android.content.ComponentName;
|
||||
@@ -741,4 +742,33 @@ public class ShortcutManager {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a single dynamic shortcut. If there are already dynamic or pinned shortcuts with the
|
||||
* same ID, each mutable shortcut is updated.
|
||||
*
|
||||
* <p>This method is useful when posting notifications which are tagged with shortcut IDs; In
|
||||
* order to make sure shortcuts exist and are up-to-date, without the need to explicitly handle
|
||||
* the shortcut count limit.
|
||||
* @see android.app.NotificationManager#notify(int, Notification)
|
||||
* @see Notification.Builder#setShortcutId(String)
|
||||
*
|
||||
* <p>If {@link #getMaxShortcutCountPerActivity()} is already reached, an existing shortcut with
|
||||
* the lowest rank will be removed to add space for the new shortcut.
|
||||
*
|
||||
* <p>If the rank of the shortcut is not explicitly set, it will be set to zero, and shortcut
|
||||
* will be added to the top of the list.
|
||||
*
|
||||
* @throws IllegalArgumentException if trying to update an immutable shortcut.
|
||||
*
|
||||
* @throws IllegalStateException when the user is locked.
|
||||
*/
|
||||
public void pushDynamicShortcut(@NonNull ShortcutInfo shortcut) {
|
||||
try {
|
||||
mService.pushDynamicShortcut(mContext.getPackageName(), shortcut, injectMyUserId());
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -309,6 +309,60 @@ class ShortcutPackage extends ShortcutPackageItem {
|
||||
forceReplaceShortcutInner(newShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a shortcut. If the max number of dynamic shortcuts is already reached, remove the
|
||||
* shortcut with the lowest rank before adding the new shortcut.
|
||||
*/
|
||||
public boolean pushDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
|
||||
Preconditions.checkArgument(newShortcut.isEnabled(),
|
||||
"pushDynamicShortcuts() cannot publish disabled shortcuts");
|
||||
|
||||
newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
|
||||
|
||||
final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
|
||||
boolean wasPinned = false;
|
||||
|
||||
if (oldShortcut == null) {
|
||||
final ShortcutService service = mShortcutUser.mService;
|
||||
final int maxShortcuts = service.getMaxActivityShortcuts();
|
||||
|
||||
final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
|
||||
sortShortcutsToActivities();
|
||||
final ArrayList<ShortcutInfo> activityShortcuts = all.get(newShortcut.getActivity());
|
||||
|
||||
if (activityShortcuts != null && activityShortcuts.size() == maxShortcuts) {
|
||||
// Max has reached. Delete the shortcut with lowest rank.
|
||||
|
||||
// Sort by isManifestShortcut() and getRank().
|
||||
Collections.sort(activityShortcuts, mShortcutTypeAndRankComparator);
|
||||
|
||||
final ShortcutInfo shortcut = activityShortcuts.get(maxShortcuts - 1);
|
||||
if (shortcut.isManifestShortcut()) {
|
||||
// All shortcuts are manifest shortcuts and cannot be removed.
|
||||
Slog.e(TAG, "Failed to remove manifest shortcut while pushing dynamic shortcut "
|
||||
+ newShortcut.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true);
|
||||
}
|
||||
} else {
|
||||
// It's an update case.
|
||||
// Make sure the target is updatable. (i.e. should be mutable.)
|
||||
oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
|
||||
|
||||
wasPinned = oldShortcut.isPinned();
|
||||
}
|
||||
|
||||
// If it was originally pinned, the new one should be pinned too.
|
||||
if (wasPinned) {
|
||||
newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
|
||||
}
|
||||
|
||||
forceReplaceShortcutInner(newShortcut);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all shortcuts that aren't pinned, cached nor dynamic.
|
||||
*/
|
||||
|
||||
@@ -133,6 +133,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -1945,6 +1946,50 @@ public class ShortcutService extends IShortcutService.Stub {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushDynamicShortcut(String packageName, ShortcutInfo shortcut,
|
||||
@UserIdInt int userId) {
|
||||
verifyCaller(packageName, userId);
|
||||
verifyShortcutInfoPackage(packageName, shortcut);
|
||||
|
||||
final boolean unlimited = injectHasUnlimitedShortcutsApiCallsPermission(
|
||||
injectBinderCallingPid(), injectBinderCallingUid());
|
||||
|
||||
synchronized (mLock) {
|
||||
throwIfUserLockedL(userId);
|
||||
|
||||
final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId);
|
||||
|
||||
ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
|
||||
fillInDefaultActivity(Arrays.asList(shortcut));
|
||||
|
||||
if (!shortcut.hasRank()) {
|
||||
shortcut.setRank(0);
|
||||
}
|
||||
// Initialize the implicit ranks for ShortcutPackage.adjustRanks().
|
||||
ps.clearAllImplicitRanks();
|
||||
shortcut.setImplicitRank(0);
|
||||
|
||||
// Validate the shortcut.
|
||||
fixUpIncomingShortcutInfo(shortcut, /* forUpdate= */ false);
|
||||
|
||||
// When ranks are changing, we need to insert between ranks, so set the
|
||||
// "rank changed" flag.
|
||||
shortcut.setRankChanged();
|
||||
|
||||
// Push it.
|
||||
if (!ps.pushDynamicShortcut(shortcut)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lastly, adjust the ranks.
|
||||
ps.adjustRanks();
|
||||
}
|
||||
packageShortcutsChanged(packageName, userId);
|
||||
|
||||
verifyStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestPinShortcut(String packageName, ShortcutInfo shortcut,
|
||||
IntentSender resultIntent, int userId) {
|
||||
|
||||
@@ -379,6 +379,113 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
|
||||
});
|
||||
}
|
||||
|
||||
public void testPushDynamicShortcut() {
|
||||
|
||||
setCaller(CALLING_PACKAGE_1, USER_0);
|
||||
|
||||
final ShortcutInfo s1 = makeShortcut("s1");
|
||||
final ShortcutInfo s2 = makeShortcut("s2");
|
||||
final ShortcutInfo s3 = makeShortcut("s3");
|
||||
final ShortcutInfo s4 = makeShortcut("s4");
|
||||
|
||||
final ShortcutInfo s10 = makeShortcut("s10");
|
||||
final ShortcutInfo s11 = makeShortcut("s11");
|
||||
final ShortcutInfo s12 = makeShortcut("s12");
|
||||
final ShortcutInfo s13 = makeShortcut("s13");
|
||||
final ShortcutInfo s14 = makeShortcut("s14");
|
||||
|
||||
// Test push as first shortcut
|
||||
mManager.pushDynamicShortcut(s1);
|
||||
assertShortcutIds(assertAllNotKeyFieldsOnly(mManager.getDynamicShortcuts()), "s1");
|
||||
assertEquals(0, getCallerShortcut("s1").getRank());
|
||||
|
||||
// Test push when other shortcuts exist
|
||||
assertTrue(mManager.setDynamicShortcuts(list(s1, s2)));
|
||||
assertShortcutIds(assertAllNotKeyFieldsOnly(mManager.getDynamicShortcuts()), "s1", "s2");
|
||||
mManager.pushDynamicShortcut(s3);
|
||||
assertShortcutIds(assertAllNotKeyFieldsOnly(mManager.getDynamicShortcuts()),
|
||||
"s1", "s2", "s3");
|
||||
assertEquals(0, getCallerShortcut("s3").getRank());
|
||||
assertEquals(1, getCallerShortcut("s1").getRank());
|
||||
assertEquals(2, getCallerShortcut("s2").getRank());
|
||||
|
||||
mInjectedCurrentTimeMillis += INTERVAL; // reset
|
||||
|
||||
// Push with set rank
|
||||
s4.setRank(2);
|
||||
mManager.pushDynamicShortcut(s4);
|
||||
assertEquals(2, getCallerShortcut("s4").getRank());
|
||||
assertEquals(3, getCallerShortcut("s2").getRank());
|
||||
|
||||
// Push existing shortcut with set rank
|
||||
final ShortcutInfo s4_2 = makeShortcut("s4");
|
||||
s4_2.setRank(4);
|
||||
mManager.pushDynamicShortcut(s4_2);
|
||||
assertEquals(2, getCallerShortcut("s2").getRank());
|
||||
assertEquals(3, getCallerShortcut("s4").getRank());
|
||||
|
||||
mInjectedCurrentTimeMillis += INTERVAL; // reset
|
||||
|
||||
// Test push as last
|
||||
assertTrue(mManager.addDynamicShortcuts(makeShortcuts("s5", "s6", "s7", "s8", "s9")));
|
||||
mManager.pushDynamicShortcut(s10);
|
||||
assertShortcutIds(assertAllNotKeyFieldsOnly(mManager.getDynamicShortcuts()),
|
||||
"s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10");
|
||||
assertEquals(0, getCallerShortcut("s10").getRank());
|
||||
assertEquals(1, getCallerShortcut("s5").getRank());
|
||||
assertEquals(6, getCallerShortcut("s3").getRank());
|
||||
assertEquals(7, getCallerShortcut("s1").getRank());
|
||||
assertEquals(8, getCallerShortcut("s2").getRank());
|
||||
assertEquals(9, getCallerShortcut("s4").getRank());
|
||||
|
||||
// Push when max has already reached
|
||||
mManager.pushDynamicShortcut(s11);
|
||||
assertShortcutIds(assertAllNotKeyFieldsOnly(mManager.getDynamicShortcuts()),
|
||||
"s1", "s2", "s3", "s5", "s6", "s7", "s8", "s9", "s10", "s11");
|
||||
assertEquals(0, getCallerShortcut("s11").getRank());
|
||||
assertEquals(1, getCallerShortcut("s10").getRank());
|
||||
assertEquals(9, getCallerShortcut("s2").getRank());
|
||||
|
||||
mInjectedCurrentTimeMillis += INTERVAL; // reset
|
||||
|
||||
// Push with different activity
|
||||
s12.setActivity(makeComponent(ShortcutActivity2.class));
|
||||
mManager.pushDynamicShortcut(s12);
|
||||
assertEquals(makeComponent(ShortcutActivity2.class),
|
||||
getCallerShortcut("s12").getActivity());
|
||||
assertEquals(0, getCallerShortcut("s12").getRank());
|
||||
|
||||
// Push to update shortcut with different activity
|
||||
final ShortcutInfo s1_2 = makeShortcut("s1");
|
||||
s1_2.setActivity(makeComponent(ShortcutActivity2.class));
|
||||
s1_2.setRank(1);
|
||||
mManager.pushDynamicShortcut(s1_2);
|
||||
assertEquals(0, getCallerShortcut("s12").getRank());
|
||||
assertEquals(1, getCallerShortcut("s1").getRank());
|
||||
assertEquals(0, getCallerShortcut("s11").getRank());
|
||||
assertEquals(1, getCallerShortcut("s10").getRank());
|
||||
assertEquals(7, getCallerShortcut("s3").getRank());
|
||||
assertEquals(8, getCallerShortcut("s2").getRank());
|
||||
|
||||
mInjectedCurrentTimeMillis += INTERVAL; // reset
|
||||
|
||||
// Test push when dropped shortcut is cached
|
||||
s13.setLongLived();
|
||||
s13.setRank(100);
|
||||
mManager.pushDynamicShortcut(s13);
|
||||
assertEquals(9, getCallerShortcut("s13").getRank());
|
||||
runWithCaller(LAUNCHER_1, USER_0, () -> {
|
||||
mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s13"), HANDLE_USER_0);
|
||||
});
|
||||
|
||||
mManager.pushDynamicShortcut(s14);
|
||||
assertShortcutIds(assertAllNotKeyFieldsOnly(mManager.getDynamicShortcuts()),
|
||||
"s1", "s2", "s3", "s5", "s6", "s7", "s8", "s9", "s10", "s11", "s12", "s14");
|
||||
// Verify s13 stayed as cached
|
||||
assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED),
|
||||
"s13");
|
||||
}
|
||||
|
||||
public void testUnlimitedCalls() {
|
||||
setCaller(CALLING_PACKAGE_1, USER_0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user