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