ShortcutPackage.java revision 2e210c4d0f766e52ea4c087a1d54213c36a4e0ea
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.server.pm;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.content.ComponentName;
21import android.content.Intent;
22import android.content.pm.ShortcutInfo;
23import android.os.PersistableBundle;
24import android.text.format.Formatter;
25import android.util.ArrayMap;
26import android.util.ArraySet;
27import android.util.Slog;
28
29import com.android.internal.annotations.VisibleForTesting;
30
31import org.xmlpull.v1.XmlPullParser;
32import org.xmlpull.v1.XmlPullParserException;
33import org.xmlpull.v1.XmlSerializer;
34
35import java.io.File;
36import java.io.IOException;
37import java.io.PrintWriter;
38import java.util.ArrayList;
39import java.util.Collection;
40import java.util.List;
41import java.util.function.Predicate;
42
43/**
44 * Package information used by {@link ShortcutService}.
45 */
46class ShortcutPackage extends ShortcutPackageItem {
47    private static final String TAG = ShortcutService.TAG;
48
49    static final String TAG_ROOT = "package";
50    private static final String TAG_INTENT_EXTRAS = "intent-extras";
51    private static final String TAG_EXTRAS = "extras";
52    private static final String TAG_SHORTCUT = "shortcut";
53
54    private static final String ATTR_NAME = "name";
55    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
56    private static final String ATTR_CALL_COUNT = "call-count";
57    private static final String ATTR_LAST_RESET = "last-reset";
58    private static final String ATTR_ID = "id";
59    private static final String ATTR_ACTIVITY = "activity";
60    private static final String ATTR_TITLE = "title";
61    private static final String ATTR_TEXT = "text";
62    private static final String ATTR_INTENT = "intent";
63    private static final String ATTR_WEIGHT = "weight";
64    private static final String ATTR_TIMESTAMP = "timestamp";
65    private static final String ATTR_FLAGS = "flags";
66    private static final String ATTR_ICON_RES = "icon-res";
67    private static final String ATTR_BITMAP_PATH = "bitmap-path";
68
69    /**
70     * All the shortcuts from the package, keyed on IDs.
71     */
72    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
73
74    /**
75     * # of dynamic shortcuts.
76     */
77    private int mDynamicShortcutCount = 0;
78
79    /**
80     * # of times the package has called rate-limited APIs.
81     */
82    private int mApiCallCount;
83
84    /**
85     * When {@link #mApiCallCount} was reset last time.
86     */
87    private long mLastResetTime;
88
89    private ShortcutPackage(int packageUserId, String packageName, ShortcutPackageInfo spi) {
90        super(packageUserId, packageName, spi != null ? spi : ShortcutPackageInfo.newEmpty());
91    }
92
93    public ShortcutPackage(int packageUserId, String packageName) {
94        this(packageUserId, packageName, null);
95    }
96
97    @Override
98    public int getOwnerUserId() {
99        // For packages, always owner user == package user.
100        return getPackageUserId();
101    }
102
103    @Override
104    protected void onRestoreBlocked(ShortcutService s) {
105        // Can't restore due to version/signature mismatch.  Remove all shortcuts.
106        mShortcuts.clear();
107    }
108
109    @Override
110    protected void onRestored(ShortcutService s) {
111        // Because some launchers may not have been restored (e.g. allowBackup=false),
112        // we need to re-calculate the pinned shortcuts.
113        refreshPinnedFlags(s);
114    }
115
116    /**
117     * Note this does *not* provide a correct view to the calling launcher.
118     */
119    @Nullable
120    public ShortcutInfo findShortcutById(String id) {
121        return mShortcuts.get(id);
122    }
123
124    private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
125            @NonNull String id) {
126        final ShortcutInfo shortcut = mShortcuts.remove(id);
127        if (shortcut != null) {
128            s.removeIcon(getPackageUserId(), shortcut);
129            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
130        }
131        return shortcut;
132    }
133
134    void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
135        deleteShortcut(s, newShortcut.getId());
136        s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
137        mShortcuts.put(newShortcut.getId(), newShortcut);
138    }
139
140    /**
141     * Add a shortcut, or update one with the same ID, with taking over existing flags.
142     *
143     * It checks the max number of dynamic shortcuts.
144     */
145    public void addDynamicShortcut(@NonNull ShortcutService s,
146            @NonNull ShortcutInfo newShortcut) {
147        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
148
149        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
150
151        final boolean wasPinned;
152        final int newDynamicCount;
153
154        if (oldShortcut == null) {
155            wasPinned = false;
156            newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
157        } else {
158            wasPinned = oldShortcut.isPinned();
159            if (oldShortcut.isDynamic()) {
160                newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
161            } else {
162                newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
163            }
164        }
165
166        // Make sure there's still room.
167        s.enforceMaxDynamicShortcuts(newDynamicCount);
168
169        // Okay, make it dynamic and add.
170        if (wasPinned) {
171            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
172        }
173
174        addShortcut(s, newShortcut);
175        mDynamicShortcutCount = newDynamicCount;
176    }
177
178    /**
179     * Remove all shortcuts that aren't pinned nor dynamic.
180     */
181    private void removeOrphans(@NonNull ShortcutService s) {
182        ArrayList<String> removeList = null; // Lazily initialize.
183
184        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
185            final ShortcutInfo si = mShortcuts.valueAt(i);
186
187            if (si.isPinned() || si.isDynamic()) continue;
188
189            if (removeList == null) {
190                removeList = new ArrayList<>();
191            }
192            removeList.add(si.getId());
193        }
194        if (removeList != null) {
195            for (int i = removeList.size() - 1; i >= 0; i--) {
196                deleteShortcut(s, removeList.get(i));
197            }
198        }
199    }
200
201    /**
202     * Remove all dynamic shortcuts.
203     */
204    public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
205        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
206            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
207        }
208        removeOrphans(s);
209        mDynamicShortcutCount = 0;
210    }
211
212    /**
213     * Remove a dynamic shortcut by ID.
214     */
215    public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
216        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
217
218        if (oldShortcut == null) {
219            return;
220        }
221        if (oldShortcut.isDynamic()) {
222            mDynamicShortcutCount--;
223        }
224        if (oldShortcut.isPinned()) {
225            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
226        } else {
227            deleteShortcut(s, shortcutId);
228        }
229    }
230
231    /**
232     * Called after a launcher updates the pinned set.  For each shortcut in this package,
233     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
234     *
235     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
236     */
237    public void refreshPinnedFlags(@NonNull ShortcutService s) {
238        // First, un-pin all shortcuts
239        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
240            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
241        }
242
243        // Then, for the pinned set for each launcher, set the pin flag one by one.
244        final ArrayMap<ShortcutUser.PackageWithUser, ShortcutLauncher> launchers =
245                s.getUserShortcutsLocked(getPackageUserId()).getAllLaunchers();
246
247        for (int l = launchers.size() - 1; l >= 0; l--) {
248            // Note even if a launcher that hasn't been installed can still pin shortcuts.
249
250            final ShortcutLauncher launcherShortcuts = launchers.valueAt(l);
251            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
252                    getPackageName(), getPackageUserId());
253
254            if (pinned == null || pinned.size() == 0) {
255                continue;
256            }
257            for (int i = pinned.size() - 1; i >= 0; i--) {
258                final String id = pinned.valueAt(i);
259                final ShortcutInfo si = mShortcuts.get(id);
260                if (si == null) {
261                    // This happens if a launcher pinned shortcuts from this package, then backup&
262                    // restored, but this package doesn't allow backing up.
263                    // In that case the launcher ends up having a dangling pinned shortcuts.
264                    // That's fine, when the launcher is restored, we'll fix it.
265                    continue;
266                }
267                si.addFlags(ShortcutInfo.FLAG_PINNED);
268            }
269        }
270
271        // Lastly, remove the ones that are no longer pinned nor dynamic.
272        removeOrphans(s);
273    }
274
275    /**
276     * Number of calls that the caller has made, since the last reset.
277     */
278    public int getApiCallCount(@NonNull ShortcutService s) {
279        final long last = s.getLastResetTimeLocked();
280
281        final long now = s.injectCurrentTimeMillis();
282        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
283            Slog.w(TAG, "Clock rewound");
284            // Clock rewound.
285            mLastResetTime = now;
286            mApiCallCount = 0;
287            return mApiCallCount;
288        }
289
290        // If not reset yet, then reset.
291        if (mLastResetTime < last) {
292            if (ShortcutService.DEBUG) {
293                Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting",
294                        mLastResetTime, now, last));
295            }
296            mApiCallCount = 0;
297            mLastResetTime = last;
298        }
299        return mApiCallCount;
300    }
301
302    /**
303     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
304     * and return true.  Otherwise just return false.
305     */
306    public boolean tryApiCall(@NonNull ShortcutService s) {
307        if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
308            return false;
309        }
310        mApiCallCount++;
311        return true;
312    }
313
314    public void resetRateLimitingForCommandLine() {
315        mApiCallCount = 0;
316        mLastResetTime = 0;
317    }
318
319    /**
320     * Find all shortcuts that match {@code query}.
321     */
322    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
323            @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
324        findAll(s, result, query, cloneFlag, null, 0);
325    }
326
327    /**
328     * Find all shortcuts that match {@code query}.
329     *
330     * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
331     * by the calling launcher will not be included in the result, and also "isPinned" will be
332     * adjusted for the caller too.
333     */
334    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
335            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
336            @Nullable String callingLauncher, int launcherUserId) {
337        if (getPackageInfo().isShadow()) {
338            // Restored and the app not installed yet, so don't return any.
339            return;
340        }
341
342        // Set of pinned shortcuts by the calling launcher.
343        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
344                : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
345                    .getPinnedShortcutIds(getPackageName(), getPackageUserId());
346
347        for (int i = 0; i < mShortcuts.size(); i++) {
348            final ShortcutInfo si = mShortcuts.valueAt(i);
349
350            // If it's called by non-launcher (i.e. publisher, always include -> true.
351            // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
352            // it.
353            final boolean isPinnedByCaller = (callingLauncher == null)
354                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
355            if (!si.isDynamic()) {
356                if (!si.isPinned()) {
357                    s.wtf("Shortcut not pinned: package " + getPackageName()
358                            + ", user=" + getPackageUserId() + ", id=" + si.getId());
359                    continue;
360                }
361                if (!isPinnedByCaller) {
362                    continue;
363                }
364            }
365            final ShortcutInfo clone = si.clone(cloneFlag);
366            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
367            // since it may check isPinned.
368            if (!isPinnedByCaller) {
369                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
370            }
371            if (query == null || query.test(clone)) {
372                result.add(clone);
373            }
374        }
375    }
376
377    public void resetThrottling() {
378        mApiCallCount = 0;
379    }
380
381    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
382        pw.println();
383
384        pw.print(prefix);
385        pw.print("Package: ");
386        pw.print(getPackageName());
387        pw.println();
388
389        pw.print(prefix);
390        pw.print("  ");
391        pw.print("Calls: ");
392        pw.print(getApiCallCount(s));
393        pw.println();
394
395        // This should be after getApiCallCount(), which may update it.
396        pw.print(prefix);
397        pw.print("  ");
398        pw.print("Last reset: [");
399        pw.print(mLastResetTime);
400        pw.print("] ");
401        pw.print(s.formatTime(mLastResetTime));
402        pw.println();
403
404        getPackageInfo().dump(s, pw, prefix + "  ");
405        pw.println();
406
407        pw.println("      Shortcuts:");
408        long totalBitmapSize = 0;
409        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
410        final int size = shortcuts.size();
411        for (int i = 0; i < size; i++) {
412            final ShortcutInfo si = shortcuts.valueAt(i);
413            pw.print("        ");
414            pw.println(si.toInsecureString());
415            if (si.getBitmapPath() != null) {
416                final long len = new File(si.getBitmapPath()).length();
417                pw.print("          ");
418                pw.print("bitmap size=");
419                pw.println(len);
420
421                totalBitmapSize += len;
422            }
423        }
424        pw.print(prefix);
425        pw.print("  ");
426        pw.print("Total bitmap size: ");
427        pw.print(totalBitmapSize);
428        pw.print(" (");
429        pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
430        pw.println(")");
431    }
432
433    @Override
434    public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
435            throws IOException, XmlPullParserException {
436        final int size = mShortcuts.size();
437
438        if (size == 0 && mApiCallCount == 0) {
439            return; // nothing to write.
440        }
441
442        out.startTag(null, TAG_ROOT);
443
444        ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
445        ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
446        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
447        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
448        getPackageInfo().saveToXml(out);
449
450        for (int j = 0; j < size; j++) {
451            saveShortcut(out, mShortcuts.valueAt(j), forBackup);
452        }
453
454        out.endTag(null, TAG_ROOT);
455    }
456
457    private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup)
458            throws IOException, XmlPullParserException {
459        if (forBackup) {
460            if (!si.isPinned()) {
461                return; // Backup only pinned icons.
462            }
463        }
464        out.startTag(null, TAG_SHORTCUT);
465        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
466        // writeAttr(out, "package", si.getPackageName()); // not needed
467        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
468        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
469        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
470        ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
471        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
472        ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight());
473        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
474                si.getLastChangedTimestamp());
475        if (forBackup) {
476            // Don't write icon information.  Also drop the dynamic flag.
477            ShortcutService.writeAttr(out, ATTR_FLAGS,
478                    si.getFlags() &
479                            ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
480                            | ShortcutInfo.FLAG_DYNAMIC));
481        } else {
482            ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
483            ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
484            ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
485        }
486
487        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
488                si.getIntentPersistableExtras());
489        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
490
491        out.endTag(null, TAG_SHORTCUT);
492    }
493
494    public static ShortcutPackage loadFromXml(ShortcutService s, XmlPullParser parser,
495            int ownerUserId, boolean fromBackup)
496            throws IOException, XmlPullParserException {
497
498        final String packageName = ShortcutService.parseStringAttribute(parser,
499                ATTR_NAME);
500
501        final ShortcutPackage ret = new ShortcutPackage(ownerUserId, packageName);
502
503        ret.mDynamicShortcutCount =
504                ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
505        ret.mApiCallCount =
506                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
507        ret.mLastResetTime =
508                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
509
510        final int outerDepth = parser.getDepth();
511        int type;
512        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
513                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
514            if (type != XmlPullParser.START_TAG) {
515                continue;
516            }
517            final int depth = parser.getDepth();
518            final String tag = parser.getName();
519            if (depth == outerDepth + 1) {
520                switch (tag) {
521                    case ShortcutPackageInfo.TAG_ROOT:
522                        ret.getPackageInfo().loadFromXml(parser, fromBackup);
523                        continue;
524                    case TAG_SHORTCUT:
525                        final ShortcutInfo si = parseShortcut(parser, packageName);
526
527                        // Don't use addShortcut(), we don't need to save the icon.
528                        ret.mShortcuts.put(si.getId(), si);
529                        continue;
530                }
531            }
532            ShortcutService.warnForInvalidTag(depth, tag);
533        }
534        return ret;
535    }
536
537    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName)
538            throws IOException, XmlPullParserException {
539        String id;
540        ComponentName activityComponent;
541        // Icon icon;
542        String title;
543        String text;
544        Intent intent;
545        PersistableBundle intentPersistableExtras = null;
546        int weight;
547        PersistableBundle extras = null;
548        long lastChangedTimestamp;
549        int flags;
550        int iconRes;
551        String bitmapPath;
552
553        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
554        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
555                ATTR_ACTIVITY);
556        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
557        text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
558        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
559        weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT);
560        lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
561        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
562        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
563        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
564
565        final int outerDepth = parser.getDepth();
566        int type;
567        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
568                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
569            if (type != XmlPullParser.START_TAG) {
570                continue;
571            }
572            final int depth = parser.getDepth();
573            final String tag = parser.getName();
574            if (ShortcutService.DEBUG_LOAD) {
575                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
576                        depth, type, tag));
577            }
578            switch (tag) {
579                case TAG_INTENT_EXTRAS:
580                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
581                    continue;
582                case TAG_EXTRAS:
583                    extras = PersistableBundle.restoreFromXml(parser);
584                    continue;
585            }
586            throw ShortcutService.throwForInvalidTag(depth, tag);
587        }
588        return new ShortcutInfo(
589                id, packageName, activityComponent, /* icon =*/ null, title, text, intent,
590                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
591                iconRes, bitmapPath);
592    }
593
594    @VisibleForTesting
595    List<ShortcutInfo> getAllShortcutsForTest() {
596        return new ArrayList<>(mShortcuts.values());
597    }
598}
599