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