Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a9f262d6 authored by Makoto Onuki's avatar Makoto Onuki Committed by Android (Google) Code Review
Browse files

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

parents cf665232 7001a615
Loading
Loading
Loading
Loading
+232 −8
Original line number Diff line number Diff line
@@ -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--) {
@@ -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");
        }
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -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() {
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -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;
                }
+76 −7
Original line number Diff line number Diff line
@@ -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));
        }
    }
}
+28 −0
Original line number Diff line number Diff line
<?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>
Loading