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:
Mehdi Alizadeh
2020-02-05 18:02:14 -08:00
parent e6d85ede20
commit 413e8fa698
7 changed files with 241 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*/

View File

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

View File

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