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