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