/*
|
* Copyright (C) 2016 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
package com.android.server.pm;
|
|
import android.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.annotation.UserIdInt;
|
import android.app.Person;
|
import android.content.ComponentName;
|
import android.content.Intent;
|
import android.content.IntentFilter;
|
import android.content.LocusId;
|
import android.content.pm.PackageInfo;
|
import android.content.pm.ShortcutInfo;
|
import android.content.pm.ShortcutManager;
|
import android.content.res.Resources;
|
import android.os.PersistableBundle;
|
import android.text.format.Formatter;
|
import android.util.ArrayMap;
|
import android.util.ArraySet;
|
import android.util.Log;
|
import android.util.Slog;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.util.ArrayUtils;
|
import com.android.internal.util.Preconditions;
|
import com.android.internal.util.XmlUtils;
|
import com.android.server.pm.ShortcutService.DumpFilter;
|
import com.android.server.pm.ShortcutService.ShortcutOperation;
|
import com.android.server.pm.ShortcutService.Stats;
|
|
import org.json.JSONException;
|
import org.json.JSONObject;
|
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlSerializer;
|
|
import java.io.File;
|
import java.io.IOException;
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.Comparator;
|
import java.util.List;
|
import java.util.Set;
|
import java.util.function.Predicate;
|
|
/**
|
* Package information used by {@link ShortcutService}.
|
* User information used by {@link ShortcutService}.
|
*
|
* All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
|
*/
|
class ShortcutPackage extends ShortcutPackageItem {
|
private static final String TAG = ShortcutService.TAG;
|
private static final String TAG_VERIFY = ShortcutService.TAG + ".verify";
|
|
static final String TAG_ROOT = "package";
|
private static final String TAG_INTENT_EXTRAS_LEGACY = "intent-extras";
|
private static final String TAG_INTENT = "intent";
|
private static final String TAG_EXTRAS = "extras";
|
private static final String TAG_SHORTCUT = "shortcut";
|
private static final String TAG_SHARE_TARGET = "share-target";
|
private static final String TAG_CATEGORIES = "categories";
|
private static final String TAG_PERSON = "person";
|
|
private static final String ATTR_NAME = "name";
|
private static final String ATTR_CALL_COUNT = "call-count";
|
private static final String ATTR_LAST_RESET = "last-reset";
|
private static final String ATTR_ID = "id";
|
private static final String ATTR_ACTIVITY = "activity";
|
private static final String ATTR_TITLE = "title";
|
private static final String ATTR_TITLE_RES_ID = "titleid";
|
private static final String ATTR_TITLE_RES_NAME = "titlename";
|
private static final String ATTR_TEXT = "text";
|
private static final String ATTR_TEXT_RES_ID = "textid";
|
private static final String ATTR_TEXT_RES_NAME = "textname";
|
private static final String ATTR_DISABLED_MESSAGE = "dmessage";
|
private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
|
private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename";
|
private static final String ATTR_DISABLED_REASON = "disabled-reason";
|
private static final String ATTR_INTENT_LEGACY = "intent";
|
private static final String ATTR_INTENT_NO_EXTRA = "intent-base";
|
private static final String ATTR_RANK = "rank";
|
private static final String ATTR_TIMESTAMP = "timestamp";
|
private static final String ATTR_FLAGS = "flags";
|
private static final String ATTR_ICON_RES_ID = "icon-res";
|
private static final String ATTR_ICON_RES_NAME = "icon-resname";
|
private static final String ATTR_BITMAP_PATH = "bitmap-path";
|
private static final String ATTR_LOCUS_ID = "locus-id";
|
|
private static final String ATTR_PERSON_NAME = "name";
|
private static final String ATTR_PERSON_URI = "uri";
|
private static final String ATTR_PERSON_KEY = "key";
|
private static final String ATTR_PERSON_IS_BOT = "is-bot";
|
private static final String ATTR_PERSON_IS_IMPORTANT = "is-important";
|
|
private static final String NAME_CATEGORIES = "categories";
|
|
private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
|
private static final String ATTR_NAME_XMLUTILS = "name";
|
|
private static final String KEY_DYNAMIC = "dynamic";
|
private static final String KEY_MANIFEST = "manifest";
|
private static final String KEY_PINNED = "pinned";
|
private static final String KEY_BITMAPS = "bitmaps";
|
private static final String KEY_BITMAP_BYTES = "bitmapBytes";
|
|
/**
|
* All the shortcuts from the package, keyed on IDs.
|
*/
|
final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
|
|
/**
|
* All the share targets from the package
|
*/
|
private final ArrayList<ShareTargetInfo> mShareTargets = new ArrayList<>(0);
|
|
/**
|
* # of times the package has called rate-limited APIs.
|
*/
|
private int mApiCallCount;
|
|
/**
|
* When {@link #mApiCallCount} was reset last time.
|
*/
|
private long mLastResetTime;
|
|
private final int mPackageUid;
|
|
private long mLastKnownForegroundElapsedTime;
|
|
private ShortcutPackage(ShortcutUser shortcutUser,
|
int packageUserId, String packageName, ShortcutPackageInfo spi) {
|
super(shortcutUser, packageUserId, packageName,
|
spi != null ? spi : ShortcutPackageInfo.newEmpty());
|
|
mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId);
|
}
|
|
public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) {
|
this(shortcutUser, packageUserId, packageName, null);
|
}
|
|
@Override
|
public int getOwnerUserId() {
|
// For packages, always owner user == package user.
|
return getPackageUserId();
|
}
|
|
public int getPackageUid() {
|
return mPackageUid;
|
}
|
|
@Nullable
|
public Resources getPackageResources() {
|
return mShortcutUser.mService.injectGetResourcesForApplicationAsUser(
|
getPackageName(), getPackageUserId());
|
}
|
|
public int getShortcutCount() {
|
return mShortcuts.size();
|
}
|
|
@Override
|
protected boolean canRestoreAnyVersion() {
|
return false;
|
}
|
|
@Override
|
protected void onRestored(int restoreBlockReason) {
|
// Shortcuts have been restored.
|
// - Unshadow all shortcuts.
|
// - Set disabled reason.
|
// - Disable if needed.
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
ShortcutInfo si = mShortcuts.valueAt(i);
|
si.clearFlags(ShortcutInfo.FLAG_SHADOW);
|
|
si.setDisabledReason(restoreBlockReason);
|
if (restoreBlockReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
|
si.addFlags(ShortcutInfo.FLAG_DISABLED);
|
}
|
}
|
// Because some launchers may not have been restored (e.g. allowBackup=false),
|
// we need to re-calculate the pinned shortcuts.
|
refreshPinnedFlags();
|
}
|
|
/**
|
* Note this does *not* provide a correct view to the calling launcher.
|
*/
|
@Nullable
|
public ShortcutInfo findShortcutById(String id) {
|
return mShortcuts.get(id);
|
}
|
|
public boolean isShortcutExistsAndInvisibleToPublisher(String id) {
|
ShortcutInfo si = findShortcutById(id);
|
return si != null && !si.isVisibleToPublisher();
|
}
|
|
public boolean isShortcutExistsAndVisibleToPublisher(String id) {
|
ShortcutInfo si = findShortcutById(id);
|
return si != null && si.isVisibleToPublisher();
|
}
|
|
private void ensureNotImmutable(@Nullable ShortcutInfo shortcut, boolean ignoreInvisible) {
|
if (shortcut != null && shortcut.isImmutable()
|
&& (!ignoreInvisible || shortcut.isVisibleToPublisher())) {
|
throw new IllegalArgumentException(
|
"Manifest shortcut ID=" + shortcut.getId()
|
+ " may not be manipulated via APIs");
|
}
|
}
|
|
public void ensureNotImmutable(@NonNull String id, boolean ignoreInvisible) {
|
ensureNotImmutable(mShortcuts.get(id), ignoreInvisible);
|
}
|
|
public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds,
|
boolean ignoreInvisible) {
|
for (int i = shortcutIds.size() - 1; i >= 0; i--) {
|
ensureNotImmutable(shortcutIds.get(i), ignoreInvisible);
|
}
|
}
|
|
public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts,
|
boolean ignoreInvisible) {
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
ensureNotImmutable(shortcuts.get(i).getId(), ignoreInvisible);
|
}
|
}
|
|
/**
|
* Delete a shortcut by ID. This will *always* remove it even if it's immutable or invisible.
|
*/
|
private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) {
|
final ShortcutInfo shortcut = mShortcuts.remove(id);
|
if (shortcut != null) {
|
mShortcutUser.mService.removeIconLocked(shortcut);
|
shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
|
| ShortcutInfo.FLAG_MANIFEST);
|
}
|
return shortcut;
|
}
|
|
/**
|
* Force replace a shortcut. If there's already a shortcut with the same ID, it'll be removed,
|
* even if it's invisible.
|
*/
|
private void forceReplaceShortcutInner(@NonNull ShortcutInfo newShortcut) {
|
final ShortcutService s = mShortcutUser.mService;
|
|
forceDeleteShortcutInner(newShortcut.getId());
|
|
// Extract Icon and update the icon res ID and the bitmap path.
|
s.saveIconAndFixUpShortcutLocked(newShortcut);
|
s.fixUpShortcutResourceNamesAndValues(newShortcut);
|
mShortcuts.put(newShortcut.getId(), newShortcut);
|
}
|
|
/**
|
* Add a shortcut. If there's already a one with the same ID, it'll be removed, even if it's
|
* invisible.
|
*
|
* It checks the max number of dynamic shortcuts.
|
*/
|
public void addOrReplaceDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
|
|
Preconditions.checkArgument(newShortcut.isEnabled(),
|
"add/setDynamicShortcuts() cannot publish disabled shortcuts");
|
|
newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
|
|
final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
|
|
final boolean wasPinned;
|
|
if (oldShortcut == null) {
|
wasPinned = false;
|
} 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);
|
}
|
|
/**
|
* Remove all shortcuts that aren't pinned nor dynamic.
|
*/
|
private void removeOrphans() {
|
ArrayList<String> removeList = null; // Lazily initialize.
|
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
|
if (si.isAlive()) continue;
|
|
if (removeList == null) {
|
removeList = new ArrayList<>();
|
}
|
removeList.add(si.getId());
|
}
|
if (removeList != null) {
|
for (int i = removeList.size() - 1; i >= 0; i--) {
|
forceDeleteShortcutInner(removeList.get(i));
|
}
|
}
|
}
|
|
/**
|
* Remove all dynamic shortcuts.
|
*/
|
public void deleteAllDynamicShortcuts(boolean ignoreInvisible) {
|
final long now = mShortcutUser.mService.injectCurrentTimeMillis();
|
|
boolean changed = false;
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (si.isDynamic() && (!ignoreInvisible || si.isVisibleToPublisher())) {
|
changed = true;
|
|
si.setTimestamp(now);
|
si.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
|
si.setRank(0); // It may still be pinned, so clear the rank.
|
}
|
}
|
if (changed) {
|
removeOrphans();
|
}
|
}
|
|
/**
|
* Remove a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
|
* is pinned, it'll remain as a pinned shortcut, and is still enabled.
|
*
|
* @return true if it's actually removed because it wasn't pinned, or false if it's still
|
* pinned.
|
*/
|
public boolean deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) {
|
final ShortcutInfo removed = deleteOrDisableWithId(
|
shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible,
|
ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
|
return removed == null;
|
}
|
|
/**
|
* Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
|
* is pinned, it'll remain as a pinned shortcut, but will be disabled.
|
*
|
* @return true if it's actually removed because it wasn't pinned, or false if it's still
|
* pinned.
|
*/
|
private boolean disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible,
|
int disabledReason) {
|
final ShortcutInfo disabled = deleteOrDisableWithId(
|
shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false, ignoreInvisible,
|
disabledReason);
|
return disabled == null;
|
}
|
|
/**
|
* Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
|
* is pinned, it'll remain as a pinned shortcut but will be disabled.
|
*/
|
public void disableWithId(@NonNull String shortcutId, String disabledMessage,
|
int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible,
|
int disabledReason) {
|
final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true,
|
overrideImmutable, ignoreInvisible, disabledReason);
|
|
if (disabled != null) {
|
if (disabledMessage != null) {
|
disabled.setDisabledMessage(disabledMessage);
|
} else if (disabledMessageResId != 0) {
|
disabled.setDisabledMessageResId(disabledMessageResId);
|
|
mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled);
|
}
|
}
|
}
|
|
@Nullable
|
private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable,
|
boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) {
|
Preconditions.checkState(
|
(disable == (disabledReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED)),
|
"disable and disabledReason disagree: " + disable + " vs " + disabledReason);
|
final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
|
|
if (oldShortcut == null || !oldShortcut.isEnabled()
|
&& (ignoreInvisible && !oldShortcut.isVisibleToPublisher())) {
|
return null; // Doesn't exist or already disabled.
|
}
|
if (!overrideImmutable) {
|
ensureNotImmutable(oldShortcut, /*ignoreInvisible=*/ true);
|
}
|
if (oldShortcut.isPinned()) {
|
|
oldShortcut.setRank(0);
|
oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST);
|
if (disable) {
|
oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED);
|
// Do not overwrite the disabled reason if one is alreay set.
|
if (oldShortcut.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
|
oldShortcut.setDisabledReason(disabledReason);
|
}
|
}
|
oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis());
|
|
// See ShortcutRequestPinProcessor.directPinShortcut().
|
if (mShortcutUser.mService.isDummyMainActivity(oldShortcut.getActivity())) {
|
oldShortcut.setActivity(null);
|
}
|
|
return oldShortcut;
|
} else {
|
forceDeleteShortcutInner(shortcutId);
|
return null;
|
}
|
}
|
|
public void enableWithId(@NonNull String shortcutId) {
|
final ShortcutInfo shortcut = mShortcuts.get(shortcutId);
|
if (shortcut != null) {
|
ensureNotImmutable(shortcut, /*ignoreInvisible=*/ true);
|
shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED);
|
shortcut.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
|
}
|
}
|
|
public void updateInvisibleShortcutForPinRequestWith(@NonNull ShortcutInfo shortcut) {
|
final ShortcutInfo source = mShortcuts.get(shortcut.getId());
|
Preconditions.checkNotNull(source);
|
|
mShortcutUser.mService.validateShortcutForPinRequest(shortcut);
|
|
shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
|
|
forceReplaceShortcutInner(shortcut);
|
|
adjustRanks();
|
}
|
|
/**
|
* Called after a launcher updates the pinned set. For each shortcut in this package,
|
* set FLAG_PINNED if any launcher has pinned it. Otherwise, clear it.
|
*
|
* <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
|
*/
|
public void refreshPinnedFlags() {
|
// First, un-pin all shortcuts
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
|
}
|
|
// Then, for the pinned set for each launcher, set the pin flag one by one.
|
mShortcutUser.forAllLaunchers(launcherShortcuts -> {
|
final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
|
getPackageName(), getPackageUserId());
|
|
if (pinned == null || pinned.size() == 0) {
|
return;
|
}
|
for (int i = pinned.size() - 1; i >= 0; i--) {
|
final String id = pinned.valueAt(i);
|
final ShortcutInfo si = mShortcuts.get(id);
|
if (si == null) {
|
// This happens if a launcher pinned shortcuts from this package, then backup&
|
// restored, but this package doesn't allow backing up.
|
// In that case the launcher ends up having a dangling pinned shortcuts.
|
// That's fine, when the launcher is restored, we'll fix it.
|
continue;
|
}
|
si.addFlags(ShortcutInfo.FLAG_PINNED);
|
}
|
});
|
|
// Lastly, remove the ones that are no longer pinned nor dynamic.
|
removeOrphans();
|
}
|
|
/**
|
* Number of calls that the caller has made, since the last reset.
|
*
|
* <p>This takes care of the resetting the counter for foreground apps as well as after
|
* locale changes.
|
*/
|
public int getApiCallCount(boolean unlimited) {
|
final ShortcutService s = mShortcutUser.mService;
|
|
// Reset the counter if:
|
// - the package is in foreground now.
|
// - the package is *not* in foreground now, but was in foreground at some point
|
// since the previous time it had been.
|
if (s.isUidForegroundLocked(mPackageUid)
|
|| (mLastKnownForegroundElapsedTime
|
< s.getUidLastForegroundElapsedTimeLocked(mPackageUid))
|
|| unlimited) {
|
mLastKnownForegroundElapsedTime = s.injectElapsedRealtime();
|
resetRateLimiting();
|
}
|
|
// Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount,
|
// but we just can't return 0 at this point, because we may have to update
|
// mLastResetTime.
|
|
final long last = s.getLastResetTimeLocked();
|
|
final long now = s.injectCurrentTimeMillis();
|
if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
|
Slog.w(TAG, "Clock rewound");
|
// Clock rewound.
|
mLastResetTime = now;
|
mApiCallCount = 0;
|
return mApiCallCount;
|
}
|
|
// If not reset yet, then reset.
|
if (mLastResetTime < last) {
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting",
|
getPackageName(), mLastResetTime, now, last));
|
}
|
mApiCallCount = 0;
|
mLastResetTime = last;
|
}
|
return mApiCallCount;
|
}
|
|
/**
|
* If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
|
* and return true. Otherwise just return false.
|
*
|
* <p>This takes care of the resetting the counter for foreground apps as well as after
|
* locale changes, which is done internally by {@link #getApiCallCount}.
|
*/
|
public boolean tryApiCall(boolean unlimited) {
|
final ShortcutService s = mShortcutUser.mService;
|
|
if (getApiCallCount(unlimited) >= s.mMaxUpdatesPerInterval) {
|
return false;
|
}
|
mApiCallCount++;
|
s.scheduleSaveUser(getOwnerUserId());
|
return true;
|
}
|
|
public void resetRateLimiting() {
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG, "resetRateLimiting: " + getPackageName());
|
}
|
if (mApiCallCount > 0) {
|
mApiCallCount = 0;
|
mShortcutUser.mService.scheduleSaveUser(getOwnerUserId());
|
}
|
}
|
|
public void resetRateLimitingForCommandLineNoSaving() {
|
mApiCallCount = 0;
|
mLastResetTime = 0;
|
}
|
|
/**
|
* Find all shortcuts that match {@code query}.
|
*/
|
public void findAll(@NonNull List<ShortcutInfo> result,
|
@Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
|
findAll(result, query, cloneFlag, null, 0, /*getPinnedByAnyLauncher=*/ false);
|
}
|
|
/**
|
* Find all shortcuts that match {@code query}.
|
*
|
* This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
|
* by the calling launcher will not be included in the result, and also "isPinned" will be
|
* adjusted for the caller too.
|
*/
|
public void findAll(@NonNull List<ShortcutInfo> result,
|
@Nullable Predicate<ShortcutInfo> query, int cloneFlag,
|
@Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher) {
|
if (getPackageInfo().isShadow()) {
|
// Restored and the app not installed yet, so don't return any.
|
return;
|
}
|
|
final ShortcutService s = mShortcutUser.mService;
|
|
// Set of pinned shortcuts by the calling launcher.
|
final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
|
: s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
|
.getPinnedShortcutIds(getPackageName(), getPackageUserId());
|
|
for (int i = 0; i < mShortcuts.size(); i++) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
|
// Need to adjust PINNED flag depending on the caller.
|
// Basically if the caller is a launcher (callingLauncher != null) and the launcher
|
// isn't pinning it, then we need to clear PINNED for this caller.
|
final boolean isPinnedByCaller = (callingLauncher == null)
|
|| ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
|
|
if (!getPinnedByAnyLauncher) {
|
if (si.isFloating()) {
|
if (!isPinnedByCaller) {
|
continue;
|
}
|
}
|
}
|
final ShortcutInfo clone = si.clone(cloneFlag);
|
|
// Fix up isPinned for the caller. Note we need to do it before the "test" callback,
|
// since it may check isPinned.
|
// However, if getPinnedByAnyLauncher is set, we do it after the test.
|
if (!getPinnedByAnyLauncher) {
|
if (!isPinnedByCaller) {
|
clone.clearFlags(ShortcutInfo.FLAG_PINNED);
|
}
|
}
|
if (query == null || query.test(clone)) {
|
if (!isPinnedByCaller) {
|
clone.clearFlags(ShortcutInfo.FLAG_PINNED);
|
}
|
result.add(clone);
|
}
|
}
|
}
|
|
public void resetThrottling() {
|
mApiCallCount = 0;
|
}
|
|
/**
|
* Returns a list of ShortcutInfos that match the given intent filter and the category of
|
* available ShareTarget definitions in this package.
|
*/
|
public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
|
@NonNull IntentFilter filter) {
|
final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
|
for (int i = 0; i < mShareTargets.size(); i++) {
|
final ShareTargetInfo target = mShareTargets.get(i);
|
for (ShareTargetInfo.TargetData data : target.mTargetData) {
|
if (filter.hasDataType(data.mMimeType)) {
|
// Matched at least with one data type
|
matchedTargets.add(target);
|
break;
|
}
|
}
|
}
|
|
if (matchedTargets.isEmpty()) {
|
return new ArrayList<>();
|
}
|
|
// Get the list of all dynamic shortcuts in this package.
|
final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
|
findAll(shortcuts, ShortcutInfo::isDynamicVisible,
|
ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION);
|
|
final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
|
for (int i = 0; i < shortcuts.size(); i++) {
|
final Set<String> categories = shortcuts.get(i).getCategories();
|
if (categories == null || categories.isEmpty()) {
|
continue;
|
}
|
for (int j = 0; j < matchedTargets.size(); j++) {
|
// Shortcut must have all of share target categories
|
boolean hasAllCategories = true;
|
final ShareTargetInfo target = matchedTargets.get(j);
|
for (int q = 0; q < target.mCategories.length; q++) {
|
if (!categories.contains(target.mCategories[q])) {
|
hasAllCategories = false;
|
break;
|
}
|
}
|
if (hasAllCategories) {
|
result.add(new ShortcutManager.ShareShortcutInfo(shortcuts.get(i),
|
new ComponentName(getPackageName(), target.mTargetClass)));
|
break;
|
}
|
}
|
}
|
return result;
|
}
|
|
public boolean hasShareTargets() {
|
return !mShareTargets.isEmpty();
|
}
|
|
/**
|
* Returns the number of shortcuts that can be used as a share target in the ShareSheet. Such
|
* shortcuts must have a matching category with at least one of the defined ShareTargets from
|
* the app's Xml resource.
|
*/
|
int getSharingShortcutCount() {
|
if (mShortcuts.isEmpty() || mShareTargets.isEmpty()) {
|
return 0;
|
}
|
|
// Get the list of all dynamic shortcuts in this package
|
final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
|
findAll(shortcuts, ShortcutInfo::isDynamicVisible, ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
|
|
int sharingShortcutCount = 0;
|
for (int i = 0; i < shortcuts.size(); i++) {
|
final Set<String> categories = shortcuts.get(i).getCategories();
|
if (categories == null || categories.isEmpty()) {
|
continue;
|
}
|
for (int j = 0; j < mShareTargets.size(); j++) {
|
// A SharingShortcut must have all of share target categories
|
boolean hasAllCategories = true;
|
final ShareTargetInfo target = mShareTargets.get(j);
|
for (int q = 0; q < target.mCategories.length; q++) {
|
if (!categories.contains(target.mCategories[q])) {
|
hasAllCategories = false;
|
break;
|
}
|
}
|
if (hasAllCategories) {
|
sharingShortcutCount++;
|
break;
|
}
|
}
|
}
|
return sharingShortcutCount;
|
}
|
|
/**
|
* Return the filenames (excluding path names) of icon bitmap files from this package.
|
*/
|
public ArraySet<String> getUsedBitmapFiles() {
|
final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size());
|
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (si.getBitmapPath() != null) {
|
usedFiles.add(getFileName(si.getBitmapPath()));
|
}
|
}
|
return usedFiles;
|
}
|
|
private static String getFileName(@NonNull String path) {
|
final int sep = path.lastIndexOf(File.separatorChar);
|
if (sep == -1) {
|
return path;
|
} else {
|
return path.substring(sep + 1);
|
}
|
}
|
|
/**
|
* @return false if any of the target activities are no longer enabled.
|
*/
|
private boolean areAllActivitiesStillEnabled() {
|
if (mShortcuts.size() == 0) {
|
return true;
|
}
|
final ShortcutService s = mShortcutUser.mService;
|
|
// Normally the number of target activities is 1 or so, so no need to use a complex
|
// structure like a set.
|
final ArrayList<ComponentName> checked = new ArrayList<>(4);
|
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
final ComponentName activity = si.getActivity();
|
|
if (checked.contains(activity)) {
|
continue; // Already checked.
|
}
|
checked.add(activity);
|
|
if ((activity != null)
|
&& !s.injectIsActivityEnabledAndExported(activity, getOwnerUserId())) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
/**
|
* Called when the package may be added or updated, or its activities may be disabled, and
|
* if so, rescan the package and do the necessary stuff.
|
*
|
* Add case:
|
* - Publish manifest shortcuts.
|
*
|
* Update case:
|
* - Re-publish manifest shortcuts.
|
* - If there are shortcuts with resources (icons or strings), update their timestamps.
|
* - Disable shortcuts whose target activities are disabled.
|
*
|
* @return TRUE if any shortcuts have been changed.
|
*/
|
public boolean rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan) {
|
final ShortcutService s = mShortcutUser.mService;
|
final long start = s.getStatStartTime();
|
|
final PackageInfo pi;
|
try {
|
pi = mShortcutUser.mService.getPackageInfo(
|
getPackageName(), getPackageUserId());
|
if (pi == null) {
|
return false; // Shouldn't happen.
|
}
|
|
if (!isNewApp && !forceRescan) {
|
// Return if the package hasn't changed, ie:
|
// - version code hasn't change
|
// - lastUpdateTime hasn't change
|
// - all target activities are still enabled.
|
|
// Note, system apps timestamps do *not* change after OTAs. (But they do
|
// after an adb sync or a local flash.)
|
// This means if a system app's version code doesn't change on an OTA,
|
// we don't notice it's updated. But that's fine since their version code *should*
|
// really change on OTAs.
|
if ((getPackageInfo().getVersionCode() == pi.getLongVersionCode())
|
&& (getPackageInfo().getLastUpdateTime() == pi.lastUpdateTime)
|
&& areAllActivitiesStillEnabled()) {
|
return false;
|
}
|
}
|
} finally {
|
s.logDurationStat(Stats.PACKAGE_UPDATE_CHECK, start);
|
}
|
|
// Now prepare to publish manifest shortcuts.
|
List<ShortcutInfo> newManifestShortcutList = null;
|
try {
|
newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService,
|
getPackageName(), getPackageUserId(), mShareTargets);
|
} catch (IOException|XmlPullParserException e) {
|
Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e);
|
}
|
final int manifestShortcutSize = newManifestShortcutList == null ? 0
|
: newManifestShortcutList.size();
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG,
|
String.format("Package %s has %d manifest shortcut(s), and %d share target(s)",
|
getPackageName(), manifestShortcutSize, mShareTargets.size()));
|
}
|
if (isNewApp && (manifestShortcutSize == 0)) {
|
// If it's a new app, and it doesn't have manifest shortcuts, then nothing to do.
|
|
// If it's an update, then it may already have manifest shortcuts, which need to be
|
// disabled.
|
return false;
|
}
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(),
|
(isNewApp ? "added" : "updated"),
|
getPackageInfo().getVersionCode(), pi.getLongVersionCode()));
|
}
|
|
getPackageInfo().updateFromPackageInfo(pi);
|
final long newVersionCode = getPackageInfo().getVersionCode();
|
|
// See if there are any shortcuts that were prevented restoring because the app was of a
|
// lower version, and re-enable them.
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (si.getDisabledReason() != ShortcutInfo.DISABLED_REASON_VERSION_LOWER) {
|
continue;
|
}
|
if (getPackageInfo().getBackupSourceVersionCode() > newVersionCode) {
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG, String.format("Shortcut %s require version %s, still not restored.",
|
si.getId(), getPackageInfo().getBackupSourceVersionCode()));
|
}
|
continue;
|
}
|
Slog.i(TAG, String.format("Restoring shortcut: %s", si.getId()));
|
si.clearFlags(ShortcutInfo.FLAG_DISABLED);
|
si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
|
}
|
|
// For existing shortcuts, update timestamps if they have any resources.
|
// Also check if shortcuts' activities are still main activities. Otherwise, disable them.
|
if (!isNewApp) {
|
Resources publisherRes = null;
|
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
|
// Disable dynamic shortcuts whose target activity is gone.
|
if (si.isDynamic()) {
|
if (si.getActivity() == null) {
|
// Note if it's dynamic, it must have a target activity, but b/36228253.
|
s.wtf("null activity detected.");
|
// TODO Maybe remove it?
|
} else if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) {
|
Slog.w(TAG, String.format(
|
"%s is no longer main activity. Disabling shorcut %s.",
|
getPackageName(), si.getId()));
|
if (disableDynamicWithId(si.getId(), /*ignoreInvisible*/ false,
|
ShortcutInfo.DISABLED_REASON_APP_CHANGED)) {
|
continue; // Actually removed.
|
}
|
// Still pinned, so fall-through and possibly update the resources.
|
}
|
}
|
|
if (si.hasAnyResources()) {
|
if (!si.isOriginallyFromManifest()) {
|
if (publisherRes == null) {
|
publisherRes = getPackageResources();
|
if (publisherRes == null) {
|
break; // Resources couldn't be loaded.
|
}
|
}
|
|
// If this shortcut is not from a manifest, then update all resource IDs
|
// from resource names. (We don't allow resource strings for
|
// non-manifest at the moment, but icons can still be resources.)
|
si.lookupAndFillInResourceIds(publisherRes);
|
}
|
si.setTimestamp(s.injectCurrentTimeMillis());
|
}
|
}
|
}
|
|
// (Re-)publish manifest shortcut.
|
publishManifestShortcuts(newManifestShortcutList);
|
|
if (newManifestShortcutList != null) {
|
pushOutExcessShortcuts();
|
}
|
|
s.verifyStates();
|
|
// This will send a notification to the launcher, and also save .
|
s.packageShortcutsChanged(getPackageName(), getPackageUserId());
|
return true; // true means changed.
|
}
|
|
private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) {
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG, String.format(
|
"Package %s: publishing manifest shortcuts", getPackageName()));
|
}
|
boolean changed = false;
|
|
// Keep the previous IDs.
|
ArraySet<String> toDisableList = null;
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
|
if (si.isManifestShortcut()) {
|
if (toDisableList == null) {
|
toDisableList = new ArraySet<>();
|
}
|
toDisableList.add(si.getId());
|
}
|
}
|
|
// Publish new ones.
|
if (newManifestShortcutList != null) {
|
final int newListSize = newManifestShortcutList.size();
|
|
for (int i = 0; i < newListSize; i++) {
|
changed = true;
|
|
final ShortcutInfo newShortcut = newManifestShortcutList.get(i);
|
final boolean newDisabled = !newShortcut.isEnabled();
|
|
final String id = newShortcut.getId();
|
final ShortcutInfo oldShortcut = mShortcuts.get(id);
|
|
boolean wasPinned = false;
|
|
if (oldShortcut != null) {
|
if (!oldShortcut.isOriginallyFromManifest()) {
|
Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId()
|
+ " exists but is not from AndroidManifest.xml, not updating.");
|
continue;
|
}
|
// Take over the pinned flag.
|
if (oldShortcut.isPinned()) {
|
wasPinned = true;
|
newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
|
}
|
}
|
if (newDisabled && !wasPinned) {
|
// If the shortcut is disabled, and it was *not* pinned, then this
|
// just doesn't have to be published.
|
// Just keep it in toDisableList, so the previous one would be removed.
|
continue;
|
}
|
|
// Note even if enabled=false, we still need to update all fields, so do it
|
// regardless.
|
forceReplaceShortcutInner(newShortcut); // This will clean up the old one too.
|
|
if (!newDisabled && toDisableList != null) {
|
// Still alive, don't remove.
|
toDisableList.remove(id);
|
}
|
}
|
}
|
|
// Disable the previous manifest shortcuts that are no longer in the manifest.
|
if (toDisableList != null) {
|
if (ShortcutService.DEBUG) {
|
Slog.d(TAG, String.format(
|
"Package %s: disabling %d stale shortcuts", getPackageName(),
|
toDisableList.size()));
|
}
|
for (int i = toDisableList.size() - 1; i >= 0; i--) {
|
changed = true;
|
|
final String id = toDisableList.valueAt(i);
|
|
disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0,
|
/* overrideImmutable=*/ true, /*ignoreInvisible=*/ false,
|
ShortcutInfo.DISABLED_REASON_APP_CHANGED);
|
}
|
removeOrphans();
|
}
|
adjustRanks();
|
return changed;
|
}
|
|
/**
|
* For each target activity, make sure # of dynamic + manifest shortcuts <= max.
|
* If too many, we'll remove the dynamic with the lowest ranks.
|
*/
|
private boolean pushOutExcessShortcuts() {
|
final ShortcutService service = mShortcutUser.mService;
|
final int maxShortcuts = service.getMaxActivityShortcuts();
|
|
boolean changed = false;
|
|
final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
|
sortShortcutsToActivities();
|
for (int outer = all.size() - 1; outer >= 0; outer--) {
|
final ArrayList<ShortcutInfo> list = all.valueAt(outer);
|
if (list.size() <= maxShortcuts) {
|
continue;
|
}
|
// Sort by isManifestShortcut() and getRank().
|
Collections.sort(list, mShortcutTypeAndRankComparator);
|
|
// Keep [0 .. max), and remove (as dynamic) [max .. size)
|
for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) {
|
final ShortcutInfo shortcut = list.get(inner);
|
|
if (shortcut.isManifestShortcut()) {
|
// This shouldn't happen -- excess shortcuts should all be non-manifest.
|
// But just in case.
|
service.wtf("Found manifest shortcuts in excess list.");
|
continue;
|
}
|
deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true);
|
}
|
}
|
|
return changed;
|
}
|
|
/**
|
* To sort by isManifestShortcut() and getRank(). i.e. manifest shortcuts come before
|
* non-manifest shortcuts, then sort by rank.
|
*
|
* This is used to decide which dynamic shortcuts to remove when an upgraded version has more
|
* manifest shortcuts than before and as a result we need to remove some of the dynamic
|
* shortcuts. We sort manifest + dynamic shortcuts by this order, and remove the ones with
|
* the last ones.
|
*
|
* (Note the number of manifest shortcuts is always <= the max number, because if there are
|
* more, ShortcutParser would ignore the rest.)
|
*/
|
final Comparator<ShortcutInfo> mShortcutTypeAndRankComparator = (ShortcutInfo a,
|
ShortcutInfo b) -> {
|
if (a.isManifestShortcut() && !b.isManifestShortcut()) {
|
return -1;
|
}
|
if (!a.isManifestShortcut() && b.isManifestShortcut()) {
|
return 1;
|
}
|
return Integer.compare(a.getRank(), b.getRank());
|
};
|
|
/**
|
* Build a list of shortcuts for each target activity and return as a map. The result won't
|
* contain "floating" shortcuts because they don't belong on any activities.
|
*/
|
private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() {
|
final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts
|
= new ArrayMap<>();
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (si.isFloating()) {
|
continue; // Ignore floating shortcuts, which are not tied to any activities.
|
}
|
|
final ComponentName activity = si.getActivity();
|
if (activity == null) {
|
mShortcutUser.mService.wtf("null activity detected.");
|
continue;
|
}
|
|
ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity);
|
if (list == null) {
|
list = new ArrayList<>();
|
activitiesToShortcuts.put(activity, list);
|
}
|
list.add(si);
|
}
|
return activitiesToShortcuts;
|
}
|
|
/** Used by {@link #enforceShortcutCountsBeforeOperation} */
|
private void incrementCountForActivity(ArrayMap<ComponentName, Integer> counts,
|
ComponentName cn, int increment) {
|
Integer oldValue = counts.get(cn);
|
if (oldValue == null) {
|
oldValue = 0;
|
}
|
|
counts.put(cn, oldValue + increment);
|
}
|
|
/**
|
* Called by
|
* {@link android.content.pm.ShortcutManager#setDynamicShortcuts},
|
* {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and
|
* {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing
|
* the operation to make sure the operation wouldn't result in the target activities having
|
* more than the allowed number of dynamic/manifest shortcuts.
|
*
|
* @param newList shortcut list passed to set, add or updateShortcuts().
|
* @param operation add, set or update.
|
* @throws IllegalArgumentException if the operation would result in going over the max
|
* shortcut count for any activity.
|
*/
|
public void enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList,
|
@ShortcutOperation int operation) {
|
final ShortcutService service = mShortcutUser.mService;
|
|
// Current # of dynamic / manifest shortcuts for each activity.
|
// (If it's for update, then don't count dynamic shortcuts, since they'll be replaced
|
// anyway.)
|
final ArrayMap<ComponentName, Integer> counts = new ArrayMap<>(4);
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo shortcut = mShortcuts.valueAt(i);
|
|
if (shortcut.isManifestShortcut()) {
|
incrementCountForActivity(counts, shortcut.getActivity(), 1);
|
} else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) {
|
incrementCountForActivity(counts, shortcut.getActivity(), 1);
|
}
|
}
|
|
for (int i = newList.size() - 1; i >= 0; i--) {
|
final ShortcutInfo newShortcut = newList.get(i);
|
final ComponentName newActivity = newShortcut.getActivity();
|
if (newActivity == null) {
|
if (operation != ShortcutService.OPERATION_UPDATE) {
|
service.wtf("Activity must not be null at this point");
|
continue; // Just ignore this invalid case.
|
}
|
continue; // Activity can be null for update.
|
}
|
|
final ShortcutInfo original = mShortcuts.get(newShortcut.getId());
|
if (original == null) {
|
if (operation == ShortcutService.OPERATION_UPDATE) {
|
continue; // When updating, ignore if there's no target.
|
}
|
// Add() or set(), and there's no existing shortcut with the same ID. We're
|
// simply publishing (as opposed to updating) this shortcut, so just +1.
|
incrementCountForActivity(counts, newActivity, 1);
|
continue;
|
}
|
if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) {
|
// Updating floating shortcuts doesn't affect the count, so ignore.
|
continue;
|
}
|
|
// If it's add() or update(), then need to decrement for the previous activity.
|
// Skip it for set() since it's already been taken care of by not counting the original
|
// dynamic shortcuts in the first loop.
|
if (operation != ShortcutService.OPERATION_SET) {
|
final ComponentName oldActivity = original.getActivity();
|
if (!original.isFloating()) {
|
incrementCountForActivity(counts, oldActivity, -1);
|
}
|
}
|
incrementCountForActivity(counts, newActivity, 1);
|
}
|
|
// Then make sure none of the activities have more than the max number of shortcuts.
|
for (int i = counts.size() - 1; i >= 0; i--) {
|
service.enforceMaxActivityShortcuts(counts.valueAt(i));
|
}
|
}
|
|
/**
|
* For all the text fields, refresh the string values if they're from resources.
|
*/
|
public void resolveResourceStrings() {
|
final ShortcutService s = mShortcutUser.mService;
|
boolean changed = false;
|
|
Resources publisherRes = null;
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
|
if (si.hasStringResources()) {
|
changed = true;
|
|
if (publisherRes == null) {
|
publisherRes = getPackageResources();
|
if (publisherRes == null) {
|
break; // Resources couldn't be loaded.
|
}
|
}
|
|
si.resolveResourceStrings(publisherRes);
|
si.setTimestamp(s.injectCurrentTimeMillis());
|
}
|
}
|
if (changed) {
|
s.packageShortcutsChanged(getPackageName(), getPackageUserId());
|
}
|
}
|
|
/** Clears the implicit ranks for all shortcuts. */
|
public void clearAllImplicitRanks() {
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
si.clearImplicitRankAndRankChangedFlag();
|
}
|
}
|
|
/**
|
* Used to sort shortcuts for rank auto-adjusting.
|
*/
|
final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> {
|
// First, sort by rank.
|
int ret = Integer.compare(a.getRank(), b.getRank());
|
if (ret != 0) {
|
return ret;
|
}
|
// When ranks are tie, then prioritize the ones that have just been assigned new ranks.
|
// e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively,
|
// adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because
|
// "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set.
|
// Similarly, updating s3's rank to 1 will insert it between s1 and s2.
|
if (a.isRankChanged() != b.isRankChanged()) {
|
return a.isRankChanged() ? -1 : 1;
|
}
|
// If they're still tie, sort by implicit rank -- i.e. preserve the order in which
|
// they're passed to the API.
|
ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank());
|
if (ret != 0) {
|
return ret;
|
}
|
// If they're still tie, just sort by their IDs.
|
// This may happen with updateShortcuts() -- see
|
// the testUpdateShortcuts_noManifestShortcuts() test.
|
return a.getId().compareTo(b.getId());
|
};
|
|
/**
|
* Re-calculate the ranks for all shortcuts.
|
*/
|
public void adjustRanks() {
|
final ShortcutService s = mShortcutUser.mService;
|
final long now = s.injectCurrentTimeMillis();
|
|
// First, clear ranks for floating shortcuts.
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (si.isFloating()) {
|
if (si.getRank() != 0) {
|
si.setTimestamp(now);
|
si.setRank(0);
|
}
|
}
|
}
|
|
// Then adjust ranks. Ranks are unique for each activity, so we first need to sort
|
// shortcuts to each activity.
|
// Then sort the shortcuts within each activity with mShortcutRankComparator, and
|
// assign ranks from 0.
|
final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
|
sortShortcutsToActivities();
|
for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity.
|
final ArrayList<ShortcutInfo> list = all.valueAt(outer);
|
|
// Sort by ranks and other signals.
|
Collections.sort(list, mShortcutRankComparator);
|
|
int rank = 0;
|
|
final int size = list.size();
|
for (int i = 0; i < size; i++) {
|
final ShortcutInfo si = list.get(i);
|
if (si.isManifestShortcut()) {
|
// Don't adjust ranks for manifest shortcuts.
|
continue;
|
}
|
// At this point, it must be dynamic.
|
if (!si.isDynamic()) {
|
s.wtf("Non-dynamic shortcut found.");
|
continue;
|
}
|
final int thisRank = rank++;
|
if (si.getRank() != thisRank) {
|
si.setTimestamp(now);
|
si.setRank(thisRank);
|
}
|
}
|
}
|
}
|
|
/** @return true if there's any shortcuts that are not manifest shortcuts. */
|
public boolean hasNonManifestShortcuts() {
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (!si.isDeclaredInManifest()) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
|
pw.println();
|
|
pw.print(prefix);
|
pw.print("Package: ");
|
pw.print(getPackageName());
|
pw.print(" UID: ");
|
pw.print(mPackageUid);
|
pw.println();
|
|
pw.print(prefix);
|
pw.print(" ");
|
pw.print("Calls: ");
|
pw.print(getApiCallCount(/*unlimited=*/ false));
|
pw.println();
|
|
// getApiCallCount() may have updated mLastKnownForegroundElapsedTime.
|
pw.print(prefix);
|
pw.print(" ");
|
pw.print("Last known FG: ");
|
pw.print(mLastKnownForegroundElapsedTime);
|
pw.println();
|
|
// This should be after getApiCallCount(), which may update it.
|
pw.print(prefix);
|
pw.print(" ");
|
pw.print("Last reset: [");
|
pw.print(mLastResetTime);
|
pw.print("] ");
|
pw.print(ShortcutService.formatTime(mLastResetTime));
|
pw.println();
|
|
getPackageInfo().dump(pw, prefix + " ");
|
pw.println();
|
|
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.println(si.toDumpString(prefix + " "));
|
if (si.getBitmapPath() != null) {
|
final long len = new File(si.getBitmapPath()).length();
|
pw.print(prefix);
|
pw.print(" ");
|
pw.print("bitmap size=");
|
pw.println(len);
|
|
totalBitmapSize += len;
|
}
|
}
|
pw.print(prefix);
|
pw.print(" ");
|
pw.print("Total bitmap size: ");
|
pw.print(totalBitmapSize);
|
pw.print(" (");
|
pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize));
|
pw.println(")");
|
}
|
|
@Override
|
public JSONObject dumpCheckin(boolean clear) throws JSONException {
|
final JSONObject result = super.dumpCheckin(clear);
|
|
int numDynamic = 0;
|
int numPinned = 0;
|
int numManifest = 0;
|
int numBitmaps = 0;
|
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);
|
|
if (si.isDynamic()) numDynamic++;
|
if (si.isDeclaredInManifest()) numManifest++;
|
if (si.isPinned()) numPinned++;
|
|
if (si.getBitmapPath() != null) {
|
numBitmaps++;
|
totalBitmapSize += new File(si.getBitmapPath()).length();
|
}
|
}
|
|
result.put(KEY_DYNAMIC, numDynamic);
|
result.put(KEY_MANIFEST, numManifest);
|
result.put(KEY_PINNED, numPinned);
|
result.put(KEY_BITMAPS, numBitmaps);
|
result.put(KEY_BITMAP_BYTES, totalBitmapSize);
|
|
// TODO Log update frequency too.
|
|
return result;
|
}
|
|
@Override
|
public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
|
throws IOException, XmlPullParserException {
|
final int size = mShortcuts.size();
|
final int shareTargetSize = mShareTargets.size();
|
|
if (size == 0 && shareTargetSize == 0 && mApiCallCount == 0) {
|
return; // nothing to write.
|
}
|
|
out.startTag(null, TAG_ROOT);
|
|
ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
|
ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
|
ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
|
getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
|
|
for (int j = 0; j < size; j++) {
|
saveShortcut(out, mShortcuts.valueAt(j), forBackup,
|
getPackageInfo().isBackupAllowed());
|
}
|
|
if (!forBackup) {
|
for (int j = 0; j < shareTargetSize; j++) {
|
mShareTargets.get(j).saveToXml(out);
|
}
|
}
|
|
out.endTag(null, TAG_ROOT);
|
}
|
|
private void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup,
|
boolean appSupportsBackup)
|
throws IOException, XmlPullParserException {
|
|
final ShortcutService s = mShortcutUser.mService;
|
|
if (forBackup) {
|
if (!(si.isPinned() && si.isEnabled())) {
|
// We only backup pinned shortcuts that are enabled.
|
// Note, this means, shortcuts that are restored but are blocked restore, e.g. due
|
// to a lower version code, will not be ported to a new device.
|
return;
|
}
|
}
|
final boolean shouldBackupDetails =
|
!forBackup // It's not backup
|
|| appSupportsBackup; // Or, it's a backup and app supports backup.
|
|
// Note: at this point no shortcuts should have bitmaps pending save, but if they do,
|
// just remove the bitmap.
|
if (si.isIconPendingSave()) {
|
s.removeIconLocked(si);
|
}
|
out.startTag(null, TAG_SHORTCUT);
|
ShortcutService.writeAttr(out, ATTR_ID, si.getId());
|
// writeAttr(out, "package", si.getPackageName()); // not needed
|
ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity());
|
// writeAttr(out, "icon", si.getIcon()); // We don't save it.
|
ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
|
ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
|
ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName());
|
ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
|
ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
|
ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName());
|
if (shouldBackupDetails) {
|
ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
|
ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID,
|
si.getDisabledMessageResourceId());
|
ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME,
|
si.getDisabledMessageResName());
|
}
|
ShortcutService.writeAttr(out, ATTR_DISABLED_REASON, si.getDisabledReason());
|
ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
|
si.getLastChangedTimestamp());
|
final LocusId locusId = si.getLocusId();
|
if (locusId != null) {
|
ShortcutService.writeAttr(out, ATTR_LOCUS_ID, si.getLocusId().getId());
|
}
|
if (forBackup) {
|
// Don't write icon information. Also drop the dynamic flag.
|
|
int flags = si.getFlags() &
|
~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
|
| ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE
|
| ShortcutInfo.FLAG_DYNAMIC);
|
ShortcutService.writeAttr(out, ATTR_FLAGS, flags);
|
|
// Set the publisher version code at every backup.
|
final long packageVersionCode = getPackageInfo().getVersionCode();
|
if (packageVersionCode == 0) {
|
s.wtf("Package version code should be available at this point.");
|
// However, 0 is a valid version code, so we just go ahead with it...
|
}
|
} else {
|
// When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored
|
// as dynamic.
|
ShortcutService.writeAttr(out, ATTR_RANK, si.getRank());
|
|
ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
|
ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId());
|
ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName());
|
ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
|
}
|
|
if (shouldBackupDetails) {
|
{
|
final Set<String> cat = si.getCategories();
|
if (cat != null && cat.size() > 0) {
|
out.startTag(null, TAG_CATEGORIES);
|
XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]),
|
NAME_CATEGORIES, out);
|
out.endTag(null, TAG_CATEGORIES);
|
}
|
}
|
if (!forBackup) { // Don't backup the persons field.
|
final Person[] persons = si.getPersons();
|
if (!ArrayUtils.isEmpty(persons)) {
|
for (int i = 0; i < persons.length; i++) {
|
final Person p = persons[i];
|
|
out.startTag(null, TAG_PERSON);
|
ShortcutService.writeAttr(out, ATTR_PERSON_NAME, p.getName());
|
ShortcutService.writeAttr(out, ATTR_PERSON_URI, p.getUri());
|
ShortcutService.writeAttr(out, ATTR_PERSON_KEY, p.getKey());
|
ShortcutService.writeAttr(out, ATTR_PERSON_IS_BOT, p.isBot());
|
ShortcutService.writeAttr(out, ATTR_PERSON_IS_IMPORTANT, p.isImportant());
|
out.endTag(null, TAG_PERSON);
|
}
|
}
|
}
|
final Intent[] intentsNoExtras = si.getIntentsNoExtras();
|
final PersistableBundle[] intentsExtras = si.getIntentPersistableExtrases();
|
final int numIntents = intentsNoExtras.length;
|
for (int i = 0; i < numIntents; i++) {
|
out.startTag(null, TAG_INTENT);
|
ShortcutService.writeAttr(out, ATTR_INTENT_NO_EXTRA, intentsNoExtras[i]);
|
ShortcutService.writeTagExtra(out, TAG_EXTRAS, intentsExtras[i]);
|
out.endTag(null, TAG_INTENT);
|
}
|
|
ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
|
}
|
|
out.endTag(null, TAG_SHORTCUT);
|
}
|
|
public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser,
|
XmlPullParser parser, boolean fromBackup)
|
throws IOException, XmlPullParserException {
|
|
final String packageName = ShortcutService.parseStringAttribute(parser,
|
ATTR_NAME);
|
|
final ShortcutPackage ret = new ShortcutPackage(shortcutUser,
|
shortcutUser.getUserId(), packageName);
|
|
ret.mApiCallCount =
|
ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
|
ret.mLastResetTime =
|
ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
|
|
|
final int outerDepth = parser.getDepth();
|
int type;
|
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
|
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
|
if (type != XmlPullParser.START_TAG) {
|
continue;
|
}
|
final int depth = parser.getDepth();
|
final String tag = parser.getName();
|
if (depth == outerDepth + 1) {
|
switch (tag) {
|
case ShortcutPackageInfo.TAG_ROOT:
|
ret.getPackageInfo().loadFromXml(parser, fromBackup);
|
|
continue;
|
case TAG_SHORTCUT:
|
final ShortcutInfo si = parseShortcut(parser, packageName,
|
shortcutUser.getUserId(), fromBackup);
|
|
// Don't use addShortcut(), we don't need to save the icon.
|
ret.mShortcuts.put(si.getId(), si);
|
continue;
|
case TAG_SHARE_TARGET:
|
ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser));
|
continue;
|
}
|
}
|
ShortcutService.warnForInvalidTag(depth, tag);
|
}
|
return ret;
|
}
|
|
private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName,
|
@UserIdInt int userId, boolean fromBackup)
|
throws IOException, XmlPullParserException {
|
String id;
|
ComponentName activityComponent;
|
// Icon icon;
|
String title;
|
int titleResId;
|
String titleResName;
|
String text;
|
int textResId;
|
String textResName;
|
String disabledMessage;
|
int disabledMessageResId;
|
String disabledMessageResName;
|
int disabledReason;
|
Intent intentLegacy;
|
PersistableBundle intentPersistableExtrasLegacy = null;
|
ArrayList<Intent> intents = new ArrayList<>();
|
int rank;
|
PersistableBundle extras = null;
|
long lastChangedTimestamp;
|
int flags;
|
int iconResId;
|
String iconResName;
|
String bitmapPath;
|
final String locusIdString;
|
int backupVersionCode;
|
ArraySet<String> categories = null;
|
ArrayList<Person> persons = new ArrayList<>();
|
|
id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
|
activityComponent = ShortcutService.parseComponentNameAttribute(parser,
|
ATTR_ACTIVITY);
|
title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
|
titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
|
titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME);
|
text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
|
textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
|
textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME);
|
disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
|
disabledMessageResId = ShortcutService.parseIntAttribute(parser,
|
ATTR_DISABLED_MESSAGE_RES_ID);
|
disabledMessageResName = ShortcutService.parseStringAttribute(parser,
|
ATTR_DISABLED_MESSAGE_RES_NAME);
|
disabledReason = ShortcutService.parseIntAttribute(parser, ATTR_DISABLED_REASON);
|
intentLegacy = ShortcutService.parseIntentAttributeNoDefault(parser, ATTR_INTENT_LEGACY);
|
rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
|
lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
|
flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
|
iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID);
|
iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME);
|
bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
|
locusIdString = ShortcutService.parseStringAttribute(parser, ATTR_LOCUS_ID);
|
|
final int outerDepth = parser.getDepth();
|
int type;
|
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
|
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
|
if (type != XmlPullParser.START_TAG) {
|
continue;
|
}
|
final int depth = parser.getDepth();
|
final String tag = parser.getName();
|
if (ShortcutService.DEBUG_LOAD) {
|
Slog.d(TAG, String.format(" depth=%d type=%d name=%s",
|
depth, type, tag));
|
}
|
switch (tag) {
|
case TAG_INTENT_EXTRAS_LEGACY:
|
intentPersistableExtrasLegacy = PersistableBundle.restoreFromXml(parser);
|
continue;
|
case TAG_INTENT:
|
intents.add(parseIntent(parser));
|
continue;
|
case TAG_EXTRAS:
|
extras = PersistableBundle.restoreFromXml(parser);
|
continue;
|
case TAG_CATEGORIES:
|
// This just contains string-array.
|
continue;
|
case TAG_PERSON:
|
persons.add(parsePerson(parser));
|
continue;
|
case TAG_STRING_ARRAY_XMLUTILS:
|
if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser,
|
ATTR_NAME_XMLUTILS))) {
|
final String[] ar = XmlUtils.readThisStringArrayXml(
|
parser, TAG_STRING_ARRAY_XMLUTILS, null);
|
categories = new ArraySet<>(ar.length);
|
for (int i = 0; i < ar.length; i++) {
|
categories.add(ar[i]);
|
}
|
}
|
continue;
|
}
|
throw ShortcutService.throwForInvalidTag(depth, tag);
|
}
|
|
if (intentLegacy != null) {
|
// For the legacy file format which supported only one intent per shortcut.
|
ShortcutInfo.setIntentExtras(intentLegacy, intentPersistableExtrasLegacy);
|
intents.clear();
|
intents.add(intentLegacy);
|
}
|
|
|
if ((disabledReason == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)
|
&& ((flags & ShortcutInfo.FLAG_DISABLED) != 0)) {
|
// We didn't used to have the disabled reason, so if a shortcut is disabled
|
// and has no reason, we assume it was disabled by publisher.
|
disabledReason = ShortcutInfo.DISABLED_REASON_BY_APP;
|
}
|
|
// All restored shortcuts are initially "shadow".
|
if (fromBackup) {
|
flags |= ShortcutInfo.FLAG_SHADOW;
|
}
|
|
final LocusId locusId = locusIdString == null ? null : new LocusId(locusIdString);
|
|
return new ShortcutInfo(
|
userId, id, packageName, activityComponent, /* icon= */ null,
|
title, titleResId, titleResName, text, textResId, textResName,
|
disabledMessage, disabledMessageResId, disabledMessageResName,
|
categories,
|
intents.toArray(new Intent[intents.size()]),
|
rank, extras, lastChangedTimestamp, flags,
|
iconResId, iconResName, bitmapPath, disabledReason,
|
persons.toArray(new Person[persons.size()]), locusId);
|
}
|
|
private static Intent parseIntent(XmlPullParser parser)
|
throws IOException, XmlPullParserException {
|
|
Intent intent = ShortcutService.parseIntentAttribute(parser,
|
ATTR_INTENT_NO_EXTRA);
|
|
final int outerDepth = parser.getDepth();
|
int type;
|
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
|
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
|
if (type != XmlPullParser.START_TAG) {
|
continue;
|
}
|
final int depth = parser.getDepth();
|
final String tag = parser.getName();
|
if (ShortcutService.DEBUG_LOAD) {
|
Slog.d(TAG, String.format(" depth=%d type=%d name=%s",
|
depth, type, tag));
|
}
|
switch (tag) {
|
case TAG_EXTRAS:
|
ShortcutInfo.setIntentExtras(intent,
|
PersistableBundle.restoreFromXml(parser));
|
continue;
|
}
|
throw ShortcutService.throwForInvalidTag(depth, tag);
|
}
|
return intent;
|
}
|
|
private static Person parsePerson(XmlPullParser parser)
|
throws IOException, XmlPullParserException {
|
CharSequence name = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_NAME);
|
String uri = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_URI);
|
String key = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_KEY);
|
boolean isBot = ShortcutService.parseBooleanAttribute(parser, ATTR_PERSON_IS_BOT);
|
boolean isImportant = ShortcutService.parseBooleanAttribute(parser,
|
ATTR_PERSON_IS_IMPORTANT);
|
|
Person.Builder builder = new Person.Builder();
|
builder.setName(name).setUri(uri).setKey(key).setBot(isBot).setImportant(isImportant);
|
return builder.build();
|
}
|
|
@VisibleForTesting
|
List<ShortcutInfo> getAllShortcutsForTest() {
|
return new ArrayList<>(mShortcuts.values());
|
}
|
|
@VisibleForTesting
|
List<ShareTargetInfo> getAllShareTargetsForTest() {
|
return new ArrayList<>(mShareTargets);
|
}
|
|
@Override
|
public void verifyStates() {
|
super.verifyStates();
|
|
boolean failed = false;
|
|
final ShortcutService s = mShortcutUser.mService;
|
|
final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
|
sortShortcutsToActivities();
|
|
// Make sure each activity won't have more than max shortcuts.
|
for (int outer = all.size() - 1; outer >= 0; outer--) {
|
final ArrayList<ShortcutInfo> list = all.valueAt(outer);
|
if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer)
|
+ " has " + all.valueAt(outer).size() + " shortcuts.");
|
}
|
|
// Sort by rank.
|
Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank()));
|
|
// Split into two arrays for each kind.
|
final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list);
|
dynamicList.removeIf((si) -> !si.isDynamic());
|
|
final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list);
|
dynamicList.removeIf((si) -> !si.isManifestShortcut());
|
|
verifyRanksSequential(dynamicList);
|
verifyRanksSequential(manifestList);
|
}
|
|
// Verify each shortcut's status.
|
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
|
final ShortcutInfo si = mShortcuts.valueAt(i);
|
if (!(si.isDeclaredInManifest() || si.isDynamic() || si.isPinned())) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " is not manifest, dynamic or pinned.");
|
}
|
if (si.isDeclaredInManifest() && si.isDynamic()) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " is both dynamic and manifest at the same time.");
|
}
|
if (si.getActivity() == null && !si.isFloating()) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " has null activity, but not floating.");
|
}
|
if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " is not floating, but is disabled.");
|
}
|
if (si.isFloating() && si.getRank() != 0) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " is floating, but has rank=" + si.getRank());
|
}
|
if (si.getIcon() != null) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " still has an icon");
|
}
|
if (si.hasAdaptiveBitmap() && !si.hasIconFile()) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " has adaptive bitmap but was not saved to a file.");
|
}
|
if (si.hasIconFile() && si.hasIconResource()) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " has both resource and bitmap icons");
|
}
|
if (si.isEnabled()
|
!= (si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " isEnabled() and getDisabledReason() disagree: "
|
+ si.isEnabled() + " vs " + si.getDisabledReason());
|
}
|
if ((si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER)
|
&& (getPackageInfo().getBackupSourceVersionCode()
|
== ShortcutInfo.VERSION_CODE_UNKNOWN)) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " RESTORED_VERSION_LOWER with no backup source version code.");
|
}
|
if (s.isDummyMainActivity(si.getActivity())) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " has a dummy target activity");
|
}
|
}
|
|
if (failed) {
|
throw new IllegalStateException("See logcat for errors");
|
}
|
}
|
|
private boolean verifyRanksSequential(List<ShortcutInfo> list) {
|
boolean failed = false;
|
|
for (int i = 0; i < list.size(); i++) {
|
final ShortcutInfo si = list.get(i);
|
if (si.getRank() != i) {
|
failed = true;
|
Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
|
+ " rank=" + si.getRank() + " but expected to be "+ i);
|
}
|
}
|
return failed;
|
}
|
}
|