Merge "ShortcutManager: Implement max # of shortcuts" into nyc-mr1-dev

This commit is contained in:
Makoto Onuki
2016-06-02 20:38:14 +00:00
committed by Android (Google) Code Review
15 changed files with 1071 additions and 56 deletions

View File

@@ -26,11 +26,13 @@ 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.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.server.pm.ShortcutService.ShortcutOperation;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -40,6 +42,8 @@ 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;
@@ -126,7 +130,8 @@ class ShortcutPackage extends ShortcutPackageItem {
}
/**
* Called when a shortcut is about to be published. At this point we know the publisher package
* Called when a shortcut is about to be published. At this point we know the publisher
* package
* exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so
* we do some initialization for the package.
*/
@@ -292,12 +297,17 @@ class ShortcutPackage extends ShortcutPackageItem {
}
/**
* Remove a dynamic shortcut by ID.
* 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.
*/
public void deleteDynamicWithId(@NonNull String shortcutId) {
deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
}
/**
* 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) {
final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true,
@@ -625,6 +635,10 @@ class ShortcutPackage extends ShortcutPackageItem {
// (Re-)publish manifest shortcut.
changed |= publishManifestShortcuts(newManifestShortcutList);
if (newManifestShortcutList != null) {
changed |= pushOutExcessShortcuts();
}
if (changed) {
// This will send a notification to the launcher, and also save .
s.packageShortcutsChanged(getPackageName(), getPackageUserId());
@@ -642,10 +656,6 @@ class ShortcutPackage extends ShortcutPackageItem {
}
boolean changed = false;
// TODO: Check dynamic count
// TODO: Kick out dynamic if too many
// Keep the previous IDs.
ArraySet<String> toDisableList = null;
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
@@ -669,7 +679,7 @@ class ShortcutPackage extends ShortcutPackageItem {
final ShortcutInfo newShortcut = newManifestShortcutList.get(i);
final boolean newDisabled = !newShortcut.isEnabled();
final String id = newShortcut.getId();
final String id = newShortcut.getId();
final ShortcutInfo oldShortcut = mShortcuts.get(id);
boolean wasPinned = false;
@@ -692,7 +702,6 @@ class ShortcutPackage extends ShortcutPackageItem {
// Just keep it in toDisableList, so the previous one would be removed.
continue;
}
// TODO: Check dynamic count
// Note even if enabled=false, we still need to update all fields, so do it
// regardless.
@@ -725,6 +734,179 @@ class ShortcutPackage extends ShortcutPackageItem {
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());
}
}
service.verifyStates();
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 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 int maxShortcuts = mShortcutUser.mService.getMaxActivityShortcuts();
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();
ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity);
if (list == null) {
list = new ArrayList<>(maxShortcuts * 2);
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("null Activity found for non-update");
}
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));
}
}
public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.println();
@@ -995,4 +1177,46 @@ class ShortcutPackage extends ShortcutPackageItem {
List<ShortcutInfo> getAllShortcutsForTest() {
return new ArrayList<>(mShortcuts.values());
}
@Override
public void verifyStates() {
super.verifyStates();
boolean failed = false;
final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
sortShortcutsToActivities();
// Make sure each activity won't have more than max shortcuts.
for (int i = all.size() - 1; i >= 0; i--) {
if (all.valueAt(i).size() > mShortcutUser.mService.getMaxActivityShortcuts()) {
failed = true;
Log.e(TAG, "Package " + getPackageName() + ": activity " + all.keyAt(i)
+ " has " + all.valueAt(i).size() + " shortcuts.");
}
}
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
final ShortcutInfo si = mShortcuts.valueAt(i);
if (!(si.isManifestShortcut() || si.isDynamic() || si.isPinned())) {
failed = true;
Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId()
+ " is not manifest, dynamic or pinned.");
}
if (si.getActivity() == null) {
failed = true;
Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId()
+ " has null activity.");
}
if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) {
failed = true;
Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId()
+ " is not floating, but is disabled.");
}
}
if (failed) {
throw new IllegalStateException("See logcat for errors");
}
}
}

View File

@@ -132,4 +132,10 @@ abstract class ShortcutPackageItem {
public abstract void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
throws IOException, XmlPullParserException;
/**
* Verify various internal states.
*/
public void verifyStates() {
}
}

View File

@@ -86,6 +86,8 @@ public class ShortcutParser {
int type;
int rank = 0;
final int maxShortcuts = service.getMaxActivityShortcuts();
int numShortcuts = 0;
outer:
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -115,10 +117,17 @@ public class ShortcutParser {
}
if (si != null) {
if (numShortcuts >= maxShortcuts) {
Slog.w(TAG, "More than " + maxShortcuts + " shortcuts found for "
+ activityInfo.getComponentName() + ", ignoring the rest.");
return result;
}
if (result == null) {
result = new ArrayList<>();
}
result.add(si);
numShortcuts++;
}
continue;
}

View File

@@ -15,6 +15,7 @@
*/
package com.android.server.pm;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -102,6 +103,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -114,8 +117,6 @@ import java.util.function.Predicate;
/**
* TODO:
* - Implement # of dynamic shortcuts cap.
*
* - HandleUnlockUser needs to be async. Wait on it in onCleanupUser.
*
* - Implement reportShortcutUsed().
@@ -137,12 +138,12 @@ import java.util.function.Predicate;
*
* - Add more call stats.
*
* - Rename getMaxDynamicShortcutCount and mMaxDynamicShortcuts
* - Rename mMaxDynamicShortcuts, because it includes manifest shortcuts too.
*/
public class ShortcutService extends IShortcutService.Stub {
static final String TAG = "ShortcutService";
static final boolean DEBUG = true; // STOPSHIP if true
static final boolean DEBUG = false; // STOPSHIP if true
static final boolean DEBUG_LOAD = false; // STOPSHIP if true
static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true
@@ -329,6 +330,19 @@ public class ShortcutService extends IShortcutService.Stub {
private static final int PROCESS_STATE_FOREGROUND_THRESHOLD =
ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
static final int OPERATION_SET = 0;
static final int OPERATION_ADD = 1;
static final int OPERATION_UPDATE = 2;
/** @hide */
@IntDef(value = {
OPERATION_SET,
OPERATION_ADD,
OPERATION_UPDATE
})
@Retention(RetentionPolicy.SOURCE)
@interface ShortcutOperation {}
public ShortcutService(Context context) {
this(context, BackgroundThread.get().getLooper());
}
@@ -1312,14 +1326,22 @@ public class ShortcutService extends IShortcutService.Stub {
}
/**
* Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
* @throws IllegalArgumentException if {@code numShortcuts} is bigger than
* {@link #getMaxActivityShortcuts()}.
*/
void enforceMaxDynamicShortcuts(int numShortcuts) {
void enforceMaxActivityShortcuts(int numShortcuts) {
if (numShortcuts > mMaxDynamicShortcuts) {
throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
}
}
/**
* Return the max number of dynamic + manifest shortcuts for each launcher icon.
*/
int getMaxActivityShortcuts() {
return mMaxDynamicShortcuts;
}
/**
* - Sends a notification to LauncherApps
* - Write to file
@@ -1440,11 +1462,12 @@ public class ShortcutService extends IShortcutService.Stub {
ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET);
// Throttling.
if (!ps.tryApiCall()) {
return false;
}
enforceMaxDynamicShortcuts(size);
// Validate the shortcuts.
for (int i = 0; i < size; i++) {
@@ -1461,6 +1484,9 @@ public class ShortcutService extends IShortcutService.Stub {
}
}
packageShortcutsChanged(packageName, userId);
verifyStates();
return true;
}
@@ -1477,6 +1503,8 @@ public class ShortcutService extends IShortcutService.Stub {
ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_UPDATE);
// Throttling.
if (!ps.tryApiCall()) {
return false;
@@ -1514,6 +1542,8 @@ public class ShortcutService extends IShortcutService.Stub {
}
packageShortcutsChanged(packageName, userId);
verifyStates();
return true;
}
@@ -1530,6 +1560,8 @@ public class ShortcutService extends IShortcutService.Stub {
ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD);
// Throttling.
if (!ps.tryApiCall()) {
return false;
@@ -1546,6 +1578,8 @@ public class ShortcutService extends IShortcutService.Stub {
}
packageShortcutsChanged(packageName, userId);
verifyStates();
return true;
}
@@ -1567,6 +1601,8 @@ public class ShortcutService extends IShortcutService.Stub {
}
}
packageShortcutsChanged(packageName, userId);
verifyStates();
}
@Override
@@ -1584,6 +1620,8 @@ public class ShortcutService extends IShortcutService.Stub {
}
}
packageShortcutsChanged(packageName, userId);
verifyStates();
}
@Override
@@ -1603,6 +1641,8 @@ public class ShortcutService extends IShortcutService.Stub {
}
}
packageShortcutsChanged(packageName, userId);
verifyStates();
}
@Override
@@ -1613,6 +1653,8 @@ public class ShortcutService extends IShortcutService.Stub {
getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts();
}
packageShortcutsChanged(packageName, userId);
verifyStates();
}
@Override
@@ -2023,6 +2065,8 @@ public class ShortcutService extends IShortcutService.Stub {
launcher.pinShortcuts(userId, packageName, shortcutIds);
}
packageShortcutsChanged(packageName, userId);
verifyStates();
}
@Override
@@ -2940,4 +2984,29 @@ public class ShortcutService extends IShortcutService.Stub {
return pkg.findShortcutById(shortcutId);
}
}
/**
* Control whether {@link #verifyStates} should be performed. We always perform it during unit
* tests.
*/
@VisibleForTesting
boolean injectShouldPerformVerification() {
return DEBUG;
}
/**
* Check various internal states and throws if there's any inconsistency.
* This is normally only enabled during unit tests.
*/
final void verifyStates() {
if (injectShouldPerformVerification()) {
verifyStatesInner();
}
}
private void verifyStatesInner() {
synchronized (this) {
forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates));
}
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="ms1-alt"
android:enabled="true"
android:shortcutIcon="@drawable/icon1"
android:shortcutShortLabel="@string/shortcut_title1"
android:shortcutLongLabel="@string/shortcut_text1"
android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
android:shortcutIntentAction="action1"
android:shortcutIntentData="data1"
/>
</shortcuts>

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="ms1"

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="ms1"

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="ms1"

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="ms1"

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="ms1_alt"
android:enabled="true"
android:shortcutIcon="@drawable/icon1"
android:shortcutShortLabel="@string/shortcut_title1"
android:shortcutLongLabel="@string/shortcut_text1"
android:shortcutDisabledMessage="@string/shortcut_disabled_message1"
android:shortcutCategories="android.shortcut.conversation:android.shortcut.media"
android:shortcutIntentAction="action1"
android:shortcutIntentData="http://a.b.c/1"
/>
<shortcut
android:shortcutId="ms2_alt"
android:enabled="true"
android:shortcutIcon="@drawable/icon2"
android:shortcutShortLabel="@string/shortcut_title2"
android:shortcutLongLabel="@string/shortcut_text2"
android:shortcutDisabledMessage="@string/shortcut_disabled_message2"
android:shortcutCategories="android.shortcut.conversation"
android:shortcutIntentAction="action2"
/>
<shortcut
android:shortcutId="ms3_alt"
android:shortcutShortLabel="@string/shortcut_title1"
android:shortcutIntentAction="android.intent.action.VIEW"
/>
<shortcut
android:shortcutId="ms4_alt"
android:shortcutShortLabel="@string/shortcut_title1"
android:shortcutIntentAction="android.intent.action.VIEW"
/>
<shortcut
android:shortcutId="ms5_alt"
android:shortcutShortLabel="@string/shortcut_title1"
android:shortcutIntentAction="android.intent.action.VIEW"
/>
</shortcuts>

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutShortLabel="@string/shortcut_title1"

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="manifest-shortcut-3"

View File

@@ -1,3 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="manifest-shortcut-3"

View File

@@ -17,37 +17,57 @@ package com.android.server.pm;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDisabled;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDynamic;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDynamicOrPinned;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllDynamicOrPinned;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllEnabled;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIcon;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIconFile;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIconResId;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIntents;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllHaveIconFile;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllHaveIconResId;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllHaveIntents;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveTitle;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllImmutable;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllKeyFieldsOnly;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllKeyFieldsOnly;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllManifest;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotHaveIntents;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotHaveTitle;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotKeyFieldsOnly;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotManifest;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllNotHaveIntents;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllNotHaveTitle;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllNotKeyFieldsOnly;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllNotManifest;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllPinned;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllStringsResolved;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertAllStringsResolved;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllUnique;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertBitmapSize;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertBundleEmpty;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCallbackNotReceived;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCallbackReceived;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCannotUpdateImmutable;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicAndPinned;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertCallbackNotReceived;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertCallbackReceived;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertCannotUpdateImmutable;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertDynamicAndPinned;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicOnly;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertDynamicShortcutCountExceeded;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertExpectException;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.cloneShortcutList;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.filterByActivity;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.findShortcut;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.hashSet;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.makeBundle;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.parceled;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.pfdToBitmap;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.resetAll;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
@@ -155,8 +175,8 @@ import java.util.function.Consumer;
* TODO More tests for pinning + manifest shortcuts
* TODO Manifest shortcuts + app upgrade -> launcher callback.
* Also locale change should trigger launcehr callbacks too, when they use strign resoucres.
* (not implemented yet.)
* Also locale change should trigger launcher callbacks too, when they use strign resoucres.
* (not implemented yet.)
* TODO: Add checks with assertAllNotHaveIcon()
* TODO: Detailed test for hasShortcutPermissionInner().
* TODO: Add tests for the command line functions too.
@@ -169,9 +189,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
* Whether to enable dump or not. Should be only true when debugging to avoid bugs where
* dump affecting the behavior.
*/
private static final boolean ENABLE_DUMP = true; // DO NOT SUBMIT WITH true
private static final boolean ENABLE_DUMP = false; // DO NOT SUBMIT WITH true
private static final boolean DUMP_IN_TEARDOWN = true; // DO NOT SUBMIT WITH true
private static final boolean DUMP_IN_TEARDOWN = false; // DO NOT SUBMIT WITH true
private static final String[] EMPTY_STRINGS = new String[0]; // Just for readability.
@@ -264,6 +284,11 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
mContext = context;
}
@Override
boolean injectShouldPerformVerification() {
return true; // Always verify during unit tests.
}
@Override
String injectShortcutManagerConstants() {
return ConfigConstants.KEY_RESET_INTERVAL_SEC + "=" + (INTERVAL / 1000) + ","
@@ -407,6 +432,25 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
protected int injectMyUserId() {
return UserHandle.getUserId(mInjectedCallingUid);
}
@Override
public boolean setDynamicShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
// Note to simulate the binder RPC, we need to clone the incoming arguments.
// Otherwise bad things will happen because they're mutable.
return super.setDynamicShortcuts(cloneShortcutList(shortcutInfoList));
}
@Override
public boolean addDynamicShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
// Note to simulate the binder RPC, we need to clone the incoming arguments.
return super.addDynamicShortcuts(cloneShortcutList(shortcutInfoList));
}
@Override
public boolean updateShortcuts(List<ShortcutInfo> shortcutInfoList) {
// Note to simulate the binder RPC, we need to clone the incoming arguments.
return super.updateShortcuts(cloneShortcutList(shortcutInfoList));
}
}
private class LauncherAppImplTestable extends LauncherAppsImpl {
@@ -585,7 +629,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
mInjectedCurrentTimeLillis = START_TIME;
mInjectedPackages = new HashMap<>();;
mInjectedPackages = new HashMap<>();
addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 1);
addPackage(CALLING_PACKAGE_2, CALLING_UID_2, 2);
addPackage(CALLING_PACKAGE_3, CALLING_UID_3, 3);
@@ -929,9 +973,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
mService.dumpInner(pw, null);
pw.close();
Log.e(TAG, "Dumping ShortcutService: " + message);
Log.v(TAG, "Dumping ShortcutService: " + message);
for (String line : out.toString().split("\n")) {
Log.e(TAG, line);
Log.v(TAG, line);
}
}
@@ -945,7 +989,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
private void dumpFileOnLogcat(String path, String message) {
if (!ENABLE_DUMP) return;
Log.i(TAG, "Dumping file: " + path + " " + message);
Log.v(TAG, "Dumping file: " + path + " " + message);
final StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line;
@@ -953,7 +997,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
Log.i(TAG, line);
}
} catch (Exception e) {
Log.e(TAG, "Couldn't read file", e);
Log.v(TAG, "Couldn't read file", e);
fail("Exception " + e);
}
}
@@ -982,7 +1026,8 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
}
private void waitOnMainThread() throws Throwable {
runTestOnUiThread(() -> {});
runTestOnUiThread(() -> {
});
}
/**
@@ -991,13 +1036,13 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
private ShortcutInfo makeShortcut(String id) {
return makeShortcut(
id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
}
private ShortcutInfo makeShortcutWithTitle(String id, String title) {
return makeShortcut(
id, title, /* activity =*/ null, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
}
/**
@@ -1006,7 +1051,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
private ShortcutInfo makeShortcutWithTimestamp(String id, long timestamp) {
final ShortcutInfo s = makeShortcut(
id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
s.setTimestamp(timestamp);
return s;
}
@@ -1018,7 +1063,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
ComponentName activity) {
final ShortcutInfo s = makeShortcut(
id, "Title-" + id, activity, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
s.setTimestamp(timestamp);
return s;
}
@@ -1029,7 +1074,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
private ShortcutInfo makeShortcutWithIcon(String id, Icon icon) {
return makeShortcut(
id, "Title-" + id, /* activity =*/ null, icon,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
}
private ShortcutInfo makePackageShortcut(String packageName, String id) {
@@ -1038,7 +1083,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
setCaller(packageName);
ShortcutInfo s = makeShortcut(
id, "Title-" + id, /* activity =*/ null, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0);
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
setCaller(origCaller); // restore the caller
return s;
@@ -1059,6 +1104,26 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
return new ShortcutInfo.Builder(mClientContext);
}
private ShortcutInfo makeShortcutWithActivity(String id, ComponentName activity) {
return makeShortcut(
id, "Title-" + id, activity, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
}
private ShortcutInfo makeShortcutWithActivityAndTitle(String id, ComponentName activity,
String title) {
return makeShortcut(
id, title, activity, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0);
}
private ShortcutInfo makeShortcutWithActivityAndRank(String id, ComponentName activity,
int rank) {
return makeShortcut(
id, "Title-" + id, activity, /* icon =*/ null,
makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), rank);
}
/**
* Make a shortcut with details.
*/
@@ -1297,6 +1362,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
i.putExtra(Intent.EXTRA_REPLACING, true);
return i;
}
private Intent genPackageDataClear(String packageName, int userId) {
Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED);
i.setData(Uri.parse("package:" + packageName));
@@ -1304,15 +1370,6 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
return i;
}
private ShortcutInfo parceled(ShortcutInfo si) {
Parcel p = Parcel.obtain();
p.writeParcelable(si, 0);
p.setDataPosition(0);
ShortcutInfo si2 = p.readParcelable(getClass().getClassLoader());
p.recycle();
return si2;
}
/**
* Test for the first launch path, no settings file available.
*/
@@ -4509,7 +4566,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
mService.checkPackageChanges(USER_P0);
assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0));
@@ -7905,4 +7962,434 @@ public class ShortcutManagerTest extends InstrumentationTestCase {
});
checkManifestShortcuts_immutable_verify();
}
/**
* Make sure the APIs won't work on manifest shortcuts.
*/
public void testManifestShortcuts_tooMany() {
// Change the max number of shortcuts.
mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
mService.handleUnlockUser(USER_0);
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_5);
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
// Only the first 3 should be published.
assertShortcutIds(mManager.getManifestShortcuts(), "ms1", "ms2", "ms3");
});
}
public void testMaxShortcutCount_set() {
// Change the max number of shortcuts.
mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1);
final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1);
final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1);
final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1);
final ShortcutInfo s1_5 = makeShortcutWithActivity("s15", a1);
final ShortcutInfo s1_6 = makeShortcutWithActivity("s16", a1);
final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2);
final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2);
final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2);
final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2);
// 3 shortcuts for 2 activities -> okay
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
mManager.removeAllDynamicShortcuts();
// 4 shortcut for activity 1 -> too many.
assertDynamicShortcutCountExceeded(() -> {
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s1_4, s2_1, s2_2, s2_3));
});
assertEmpty(mManager.getDynamicShortcuts());
// 4 shortcut for activity 2 -> too many.
assertDynamicShortcutCountExceeded(() -> {
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3, s2_4));
});
assertEmpty(mManager.getDynamicShortcuts());
// First, set 3. Then set 4, which should be ignored.
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13");
assertDynamicShortcutCountExceeded(() -> {
mManager.setDynamicShortcuts(list(s2_1, s2_2, s2_3, s2_4));
});
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13");
// Set will remove the old dynamic set, unlike add, so the following should pass.
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13");
mManager.setDynamicShortcuts(list(s1_4, s1_5, s1_6));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s14", "s15", "s16");
// Now, test with 2 manifest shortcuts.
mManager.removeAllDynamicShortcuts();
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_2);
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
assertEquals(2, mManager.getManifestShortcuts().size());
// Setting 1 to activity 1 will work.
mManager.setDynamicShortcuts(list(s1_1, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s21", "s22", "s23");
assertEquals(2, mManager.getManifestShortcuts().size());
// But setting 2 will not.
mManager.removeAllDynamicShortcuts();
assertDynamicShortcutCountExceeded(() -> {
mManager.setDynamicShortcuts(list(s1_1, s1_2, s2_1, s2_2, s2_3));
});
assertEmpty(mManager.getDynamicShortcuts());
assertEquals(2, mManager.getManifestShortcuts().size());
});
}
public void testMaxShortcutCount_add() {
// Change the max number of shortcuts.
mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1);
final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1);
final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1);
final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1);
final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2);
final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2);
final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2);
final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2);
// 3 shortcuts for 2 activities -> okay
mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
mManager.removeAllDynamicShortcuts();
;
// 4 shortcut for activity 1 -> too many.
assertDynamicShortcutCountExceeded(() -> {
mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s1_4, s2_1, s2_2, s2_3));
});
assertEmpty(mManager.getDynamicShortcuts());
// 4 shortcut for activity 2 -> too many.
assertDynamicShortcutCountExceeded(() -> {
mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3, s2_4));
});
assertEmpty(mManager.getDynamicShortcuts());
// First, set 3. Then add 1 more, which should be ignored.
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13");
assertDynamicShortcutCountExceeded(() -> {
mManager.addDynamicShortcuts(list(s1_4, s2_1));
});
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13");
// Update existing one, which should work.
mManager.addDynamicShortcuts(list(makeShortcutWithActivityAndTitle(
"s11", a1, "xxx"), s2_1));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21");
assertEquals("xxx", getCallerShortcut("s11").getTitle());
// Make sure pinned shortcuts won't affect.
// - Pin s11 - s13, and remove all dynamic.
runWithCaller(LAUNCHER_1, USER_0, () -> {
mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s13"),
HANDLE_USER_0);
});
mManager.removeAllDynamicShortcuts();
assertEmpty(mManager.getDynamicShortcuts());
assertShortcutIds(mManager.getPinnedShortcuts(),
"s11", "s12", "s13");
// Then add dynamic.
mManager.addDynamicShortcuts(list(s1_4, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s14", "s21", "s22", "s23");
assertShortcutIds(mManager.getPinnedShortcuts(),
"s11", "s12", "s13");
// Adding "s11" and "s12" back, should work
mManager.addDynamicShortcuts(list(s1_1, s1_2));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s14", "s11", "s12", "s21", "s22", "s23");
assertShortcutIds(mManager.getPinnedShortcuts(),
"s11", "s12", "s13");
// Adding back s13 doesn't work.
assertDynamicShortcutCountExceeded(() -> {
mManager.addDynamicShortcuts(list(s1_3));
});
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
"s11", "s12", "s14");
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
"s21", "s22", "s23");
// Now swap the activities.
mManager.updateShortcuts(list(
makeShortcutWithActivity("s11", a2),
makeShortcutWithActivity("s21", a1)));
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
"s21", "s12", "s14");
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
"s11", "s22", "s23");
// Now, test with 2 manifest shortcuts.
mManager.removeAllDynamicShortcuts();
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_2);
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
assertEquals(2, mManager.getManifestShortcuts().size());
// Adding one shortcut to activity 1 works fine.
mManager.addDynamicShortcuts(list(s1_1, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s21", "s22", "s23");
assertEquals(2, mManager.getManifestShortcuts().size());
// But adding one more doesn't.
assertDynamicShortcutCountExceeded(() -> {
mManager.addDynamicShortcuts(list(s1_4, s2_1));
});
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s21", "s22", "s23");
assertEquals(2, mManager.getManifestShortcuts().size());
});
}
public void testMaxShortcutCount_update() {
// Change the max number of shortcuts.
mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1);
final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1);
final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1);
final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1);
final ShortcutInfo s1_5 = makeShortcutWithActivity("s15", a1);
final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2);
final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2);
final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2);
final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2);
// 3 shortcuts for 2 activities -> okay
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
// Trying to move s11 from a1 to a2 should fail.
assertDynamicShortcutCountExceeded(() -> {
mManager.updateShortcuts(list(makeShortcutWithActivity("s11", a2)));
});
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
// Trying to move s21 from a2 to a1 should also fail.
assertDynamicShortcutCountExceeded(() -> {
mManager.updateShortcuts(list(makeShortcutWithActivity("s21", a1)));
});
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
// But, if we do these two at the same time, it should work.
mManager.updateShortcuts(list(
makeShortcutWithActivity("s11", a2),
makeShortcutWithActivity("s21", a1)));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
"s21", "s12", "s13");
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
"s11", "s22", "s23");
// Then reset.
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s11", "s12", "s13", "s21", "s22", "s23");
// Pin some to have more shortcuts for a1.
runWithCaller(LAUNCHER_1, USER_0, () -> {
mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s13"),
HANDLE_USER_0);
});
mManager.setDynamicShortcuts(list(s1_4, s1_5, s2_1, s2_2, s2_3));
assertShortcutIds(mManager.getDynamicShortcuts(),
"s14", "s15", "s21", "s22", "s23");
assertShortcutIds(mManager.getPinnedShortcuts(),
"s11", "s12", "s13");
// a1 already has 2 dynamic shortcuts (and 3 pinned shortcuts that used to belong on it)
// But that doesn't matter for update -- the following should still work.
mManager.updateShortcuts(list(
makeShortcutWithActivityAndTitle("s11", a1, "xxx1"),
makeShortcutWithActivityAndTitle("s12", a1, "xxx2"),
makeShortcutWithActivityAndTitle("s13", a1, "xxx3"),
makeShortcutWithActivityAndTitle("s14", a1, "xxx4"),
makeShortcutWithActivityAndTitle("s15", a1, "xxx5")));
// All the shortcuts should still exist they all belong on same activities,
// with the updated titles.
assertShortcutIds(mManager.getDynamicShortcuts(),
"s14", "s15", "s21", "s22", "s23");
assertShortcutIds(mManager.getPinnedShortcuts(),
"s11", "s12", "s13");
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1),
"s14", "s15");
assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2),
"s21", "s22", "s23");
assertEquals("xxx1", getCallerShortcut("s11").getTitle());
assertEquals("xxx2", getCallerShortcut("s12").getTitle());
assertEquals("xxx3", getCallerShortcut("s13").getTitle());
assertEquals("xxx4", getCallerShortcut("s14").getTitle());
assertEquals("xxx5", getCallerShortcut("s15").getTitle());
});
}
public void testShortcutsPushedOutByManifest() {
// Change the max number of shortcuts.
mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class);
final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class);
final ShortcutInfo s1_1 = makeShortcutWithActivityAndRank("s11", a1, 4);
final ShortcutInfo s1_2 = makeShortcutWithActivityAndRank("s12", a1, 3);
final ShortcutInfo s1_3 = makeShortcutWithActivityAndRank("s13", a1, 2);
final ShortcutInfo s1_4 = makeShortcutWithActivityAndRank("s14", a1, 1);
final ShortcutInfo s1_5 = makeShortcutWithActivityAndRank("s15", a1, 0);
final ShortcutInfo s2_1 = makeShortcutWithActivityAndRank("s21", a2, 0);
final ShortcutInfo s2_2 = makeShortcutWithActivityAndRank("s22", a2, 1);
final ShortcutInfo s2_3 = makeShortcutWithActivityAndRank("s23", a2, 2);
final ShortcutInfo s2_4 = makeShortcutWithActivityAndRank("s24", a2, 3);
final ShortcutInfo s2_5 = makeShortcutWithActivityAndRank("s25", a2, 4);
// Initial state.
mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3));
runWithCaller(LAUNCHER_1, USER_0, () -> {
mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s21", "s22"),
HANDLE_USER_0);
});
mManager.setDynamicShortcuts(list(s1_2, s1_3, s1_4, s2_2, s2_3, s2_4));
assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
"s12", "s13", "s14",
"s22", "s23", "s24");
assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
"s11", "s12",
"s21", "s22");
// Add 1 manifest shortcut to a1.
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_1);
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
assertEquals(1, mManager.getManifestShortcuts().size());
// s12 removed.
assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
"s13", "s14",
"s22", "s23", "s24");
assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
"s11", "s12",
"s21", "s22");
// Add more manifest shortcuts.
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_2);
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
R.xml.shortcut_1_alt);
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
assertEquals(3, mManager.getManifestShortcuts().size());
// Note the ones with the highest rank values (== least important) will be removed.
assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
"s14",
"s22", "s23");
assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
"s11", "s12",
"s21", "s22");
// Add more manifest shortcuts.
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_2);
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
R.xml.shortcut_5_alt); // manifest has 5, but max is 3, so a2 will have 3.
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
assertEquals(5, mManager.getManifestShortcuts().size());
assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
"s14" // a1 has 1 dynamic
); // a2 has no dynamic
assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
"s11", "s12",
"s21", "s22");
// Update, no manifest shortucts. This doesn't affect anything.
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_0);
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()),
R.xml.shortcut_0);
updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
assertEquals(0, mManager.getManifestShortcuts().size());
assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()),
"s14");
assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()),
"s11", "s12",
"s21", "s22");
});
}
}

View File

@@ -31,6 +31,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
@@ -38,6 +39,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.BaseBundle;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.test.MoreAsserts;
@@ -252,6 +254,17 @@ public class ShortcutManagerTestUtils {
return list;
}
public static List<ShortcutInfo> filterByActivity(List<ShortcutInfo> list,
ComponentName activity) {
final ArrayList<ShortcutInfo> ret = new ArrayList<>();
for (ShortcutInfo si : list) {
if (si.getActivity().equals(activity) && (si.isManifestShortcut() || si.isDynamic())) {
ret.add(si);
}
}
return ret;
}
public static void assertExpectException(Class<? extends Throwable> expectedExceptionType,
String expectedExceptionMessageRegex, Runnable r) {
assertExpectException("", expectedExceptionType, expectedExceptionMessageRegex, r);
@@ -555,6 +568,27 @@ public class ShortcutManagerTestUtils {
}, "Shortcut IDs=[" + Arrays.toString(ids) + "]", null);
}
public static ShortcutInfo parceled(ShortcutInfo si) {
Parcel p = Parcel.obtain();
p.writeParcelable(si, 0);
p.setDataPosition(0);
ShortcutInfo si2 = p.readParcelable(ShortcutManagerTestUtils.class.getClassLoader());
p.recycle();
return si2;
}
public static List<ShortcutInfo> cloneShortcutList(List<ShortcutInfo> list) {
if (list == null) {
return null;
}
final List<ShortcutInfo> ret = new ArrayList<>(list.size());
for (ShortcutInfo si : list) {
ret.add(parceled(si));
}
return ret;
}
public static void waitUntil(String message, BooleanSupplier condition) {
waitUntil(message, condition, STANDARD_TIMEOUT_SEC);
}