ShortcutService.java revision aa8b94aa79df94d77d0f7698ce532ac98f197a99
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.app.ActivityManager;
22import android.content.ComponentName;
23import android.content.ContentProvider;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.IShortcutService;
27import android.content.pm.LauncherApps;
28import android.content.pm.LauncherApps.ShortcutQuery;
29import android.content.pm.PackageManager;
30import android.content.pm.PackageManager.NameNotFoundException;
31import android.content.pm.PackageManagerInternal;
32import android.content.pm.ParceledListSlice;
33import android.content.pm.ResolveInfo;
34import android.content.pm.ShortcutInfo;
35import android.content.pm.ShortcutServiceInternal;
36import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
37import android.graphics.Bitmap;
38import android.graphics.Bitmap.CompressFormat;
39import android.graphics.BitmapFactory;
40import android.graphics.Canvas;
41import android.graphics.RectF;
42import android.graphics.drawable.Icon;
43import android.net.Uri;
44import android.os.Binder;
45import android.os.Environment;
46import android.os.Handler;
47import android.os.Looper;
48import android.os.ParcelFileDescriptor;
49import android.os.PersistableBundle;
50import android.os.Process;
51import android.os.RemoteException;
52import android.os.ResultReceiver;
53import android.os.SELinux;
54import android.os.ShellCommand;
55import android.os.UserHandle;
56import android.text.TextUtils;
57import android.text.format.Formatter;
58import android.text.format.Time;
59import android.util.ArrayMap;
60import android.util.ArraySet;
61import android.util.AtomicFile;
62import android.util.KeyValueListParser;
63import android.util.Slog;
64import android.util.SparseArray;
65import android.util.TypedValue;
66import android.util.Xml;
67
68import com.android.internal.annotations.GuardedBy;
69import com.android.internal.annotations.VisibleForTesting;
70import com.android.internal.os.BackgroundThread;
71import com.android.internal.util.FastXmlSerializer;
72import com.android.internal.util.Preconditions;
73import com.android.server.LocalServices;
74import com.android.server.SystemService;
75
76import libcore.io.IoUtils;
77import libcore.util.Objects;
78
79import org.xmlpull.v1.XmlPullParser;
80import org.xmlpull.v1.XmlPullParserException;
81import org.xmlpull.v1.XmlSerializer;
82
83import java.io.File;
84import java.io.FileDescriptor;
85import java.io.FileInputStream;
86import java.io.FileNotFoundException;
87import java.io.FileOutputStream;
88import java.io.IOException;
89import java.io.InputStream;
90import java.io.PrintWriter;
91import java.net.URISyntaxException;
92import java.nio.charset.StandardCharsets;
93import java.util.ArrayList;
94import java.util.List;
95import java.util.function.Predicate;
96
97/**
98 * TODO:
99 *
100 * - Default launcher check does take a few ms.  Worth caching.
101 *
102 * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res
103 *   -> Need to scan all packages when a user starts too.
104 *   -> Clear data -> remove all dynamic?  but not the pinned?
105 *
106 * - Scan and remove orphan bitmaps (just in case).
107 *
108 * - Backup & restore
109 *
110 * - Detect when already registered instances are passed to APIs again, which might break
111 *   internal bitmap handling.
112 */
113public class ShortcutService extends IShortcutService.Stub {
114    static final String TAG = "ShortcutService";
115
116    static final boolean DEBUG = false; // STOPSHIP if true
117    static final boolean DEBUG_LOAD = false; // STOPSHIP if true
118
119    @VisibleForTesting
120    static final long DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day
121
122    @VisibleForTesting
123    static final int DEFAULT_MAX_DAILY_UPDATES = 10;
124
125    @VisibleForTesting
126    static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5;
127
128    @VisibleForTesting
129    static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
130
131    @VisibleForTesting
132    static final int DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP = 48;
133
134    @VisibleForTesting
135    static final String DEFAULT_ICON_PERSIST_FORMAT = CompressFormat.PNG.name();
136
137    @VisibleForTesting
138    static final int DEFAULT_ICON_PERSIST_QUALITY = 100;
139
140    @VisibleForTesting
141    static final int DEFAULT_SAVE_DELAY_MS = 3000;
142
143    @VisibleForTesting
144    static final String FILENAME_BASE_STATE = "shortcut_service.xml";
145
146    @VisibleForTesting
147    static final String DIRECTORY_PER_USER = "shortcut_service";
148
149    @VisibleForTesting
150    static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
151
152    static final String DIRECTORY_BITMAPS = "bitmaps";
153
154    private static final String TAG_ROOT = "root";
155    private static final String TAG_LAST_RESET_TIME = "last_reset_time";
156
157    private static final String ATTR_VALUE = "value";
158
159    @VisibleForTesting
160    interface ConfigConstants {
161        /**
162         * Key name for the save delay, in milliseconds. (int)
163         */
164        String KEY_SAVE_DELAY_MILLIS = "save_delay_ms";
165
166        /**
167         * Key name for the throttling reset interval, in seconds. (long)
168         */
169        String KEY_RESET_INTERVAL_SEC = "reset_interval_sec";
170
171        /**
172         * Key name for the max number of modifying API calls per app for every interval. (int)
173         */
174        String KEY_MAX_DAILY_UPDATES = "max_daily_updates";
175
176        /**
177         * Key name for the max icon dimensions in DP, for non-low-memory devices.
178         */
179        String KEY_MAX_ICON_DIMENSION_DP = "max_icon_dimension_dp";
180
181        /**
182         * Key name for the max icon dimensions in DP, for low-memory devices.
183         */
184        String KEY_MAX_ICON_DIMENSION_DP_LOWRAM = "max_icon_dimension_dp_lowram";
185
186        /**
187         * Key name for the max dynamic shortcuts per app. (int)
188         */
189        String KEY_MAX_SHORTCUTS = "max_shortcuts";
190
191        /**
192         * Key name for icon compression quality, 0-100.
193         */
194        String KEY_ICON_QUALITY = "icon_quality";
195
196        /**
197         * Key name for icon compression format: "PNG", "JPEG" or "WEBP"
198         */
199        String KEY_ICON_FORMAT = "icon_format";
200    }
201
202    final Context mContext;
203
204    private final Object mLock = new Object();
205
206    private final Handler mHandler;
207
208    @GuardedBy("mLock")
209    private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1);
210
211    @GuardedBy("mLock")
212    private long mRawLastResetTime;
213
214    /**
215     * User ID -> UserShortcuts
216     */
217    @GuardedBy("mLock")
218    private final SparseArray<UserShortcuts> mUsers = new SparseArray<>();
219
220    /**
221     * Max number of dynamic shortcuts that each application can have at a time.
222     */
223    private int mMaxDynamicShortcuts;
224
225    /**
226     * Max number of updating API calls that each application can make a day.
227     */
228    int mMaxDailyUpdates;
229
230    /**
231     * Actual throttling-reset interval.  By default it's a day.
232     */
233    private long mResetInterval;
234
235    /**
236     * Icon max width/height in pixels.
237     */
238    private int mMaxIconDimension;
239
240    private CompressFormat mIconPersistFormat;
241    private int mIconPersistQuality;
242
243    private int mSaveDelayMillis;
244
245    private final PackageManagerInternal mPackageManagerInternal;
246
247    @GuardedBy("mLock")
248    private List<Integer> mDirtyUserIds = new ArrayList<>();
249
250    public ShortcutService(Context context) {
251        this(context, BackgroundThread.get().getLooper());
252    }
253
254    @VisibleForTesting
255    ShortcutService(Context context, Looper looper) {
256        mContext = Preconditions.checkNotNull(context);
257        LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
258        mHandler = new Handler(looper);
259        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
260    }
261
262    /**
263     * System service lifecycle.
264     */
265    public static final class Lifecycle extends SystemService {
266        final ShortcutService mService;
267
268        public Lifecycle(Context context) {
269            super(context);
270            mService = new ShortcutService(context);
271        }
272
273        @Override
274        public void onStart() {
275            publishBinderService(Context.SHORTCUT_SERVICE, mService);
276        }
277
278        @Override
279        public void onBootPhase(int phase) {
280            mService.onBootPhase(phase);
281        }
282
283        @Override
284        public void onCleanupUser(int userHandle) {
285            synchronized (mService.mLock) {
286                mService.onCleanupUserLocked(userHandle);
287            }
288        }
289
290        @Override
291        public void onUnlockUser(int userId) {
292            synchronized (mService.mLock) {
293                mService.onStartUserLocked(userId);
294            }
295        }
296    }
297
298    /** lifecycle event */
299    void onBootPhase(int phase) {
300        if (DEBUG) {
301            Slog.d(TAG, "onBootPhase: " + phase);
302        }
303        switch (phase) {
304            case SystemService.PHASE_LOCK_SETTINGS_READY:
305                initialize();
306                break;
307        }
308    }
309
310    /** lifecycle event */
311    void onStartUserLocked(int userId) {
312        // Preload
313        getUserShortcutsLocked(userId);
314    }
315
316    /** lifecycle event */
317    void onCleanupUserLocked(int userId) {
318        // Save all dirty information.
319        saveDirtyInfo();
320
321        // Unload
322        mUsers.delete(userId);
323    }
324
325    /** Return the base state file name */
326    private AtomicFile getBaseStateFile() {
327        final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE);
328        path.mkdirs();
329        return new AtomicFile(path);
330    }
331
332    /**
333     * Init the instance. (load the state file, etc)
334     */
335    private void initialize() {
336        synchronized (mLock) {
337            loadConfigurationLocked();
338            loadBaseStateLocked();
339        }
340    }
341
342    /**
343     * Load the configuration from Settings.
344     */
345    private void loadConfigurationLocked() {
346        updateConfigurationLocked(injectShortcutManagerConstants());
347    }
348
349    /**
350     * Load the configuration from Settings.
351     */
352    @VisibleForTesting
353    boolean updateConfigurationLocked(String config) {
354        boolean result = true;
355
356        final KeyValueListParser parser = new KeyValueListParser(',');
357        try {
358            parser.setString(config);
359        } catch (IllegalArgumentException e) {
360            // Failed to parse the settings string, log this and move on
361            // with defaults.
362            Slog.e(TAG, "Bad shortcut manager settings", e);
363            result = false;
364        }
365
366        mSaveDelayMillis = (int) parser.getLong(ConfigConstants.KEY_SAVE_DELAY_MILLIS,
367                DEFAULT_SAVE_DELAY_MS);
368
369        mResetInterval = parser.getLong(
370                ConfigConstants.KEY_RESET_INTERVAL_SEC, DEFAULT_RESET_INTERVAL_SEC)
371                * 1000L;
372
373        mMaxDailyUpdates = (int) parser.getLong(
374                ConfigConstants.KEY_MAX_DAILY_UPDATES, DEFAULT_MAX_DAILY_UPDATES);
375
376        mMaxDynamicShortcuts = (int) parser.getLong(
377                ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_APP);
378
379        final int iconDimensionDp = injectIsLowRamDevice()
380                ? (int) parser.getLong(
381                    ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
382                    DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
383                : (int) parser.getLong(
384                    ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
385                    DEFAULT_MAX_ICON_DIMENSION_DP);
386
387        mMaxIconDimension = injectDipToPixel(iconDimensionDp);
388
389        mIconPersistFormat = CompressFormat.valueOf(
390                parser.getString(ConfigConstants.KEY_ICON_FORMAT, DEFAULT_ICON_PERSIST_FORMAT));
391
392        mIconPersistQuality = (int) parser.getLong(
393                ConfigConstants.KEY_ICON_QUALITY,
394                DEFAULT_ICON_PERSIST_QUALITY);
395
396        return result;
397    }
398
399    @VisibleForTesting
400    String injectShortcutManagerConstants() {
401        return android.provider.Settings.Global.getString(
402                mContext.getContentResolver(),
403                android.provider.Settings.Global.SHORTCUT_MANAGER_CONSTANTS);
404    }
405
406    @VisibleForTesting
407    int injectDipToPixel(int dip) {
408        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
409                mContext.getResources().getDisplayMetrics());
410    }
411
412    // === Persisting ===
413
414    @Nullable
415    static String parseStringAttribute(XmlPullParser parser, String attribute) {
416        return parser.getAttributeValue(null, attribute);
417    }
418
419    static int parseIntAttribute(XmlPullParser parser, String attribute) {
420        return (int) parseLongAttribute(parser, attribute);
421    }
422
423    static long parseLongAttribute(XmlPullParser parser, String attribute) {
424        final String value = parseStringAttribute(parser, attribute);
425        if (TextUtils.isEmpty(value)) {
426            return 0;
427        }
428        try {
429            return Long.parseLong(value);
430        } catch (NumberFormatException e) {
431            Slog.e(TAG, "Error parsing long " + value);
432            return 0;
433        }
434    }
435
436    @Nullable
437    static ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) {
438        final String value = parseStringAttribute(parser, attribute);
439        if (TextUtils.isEmpty(value)) {
440            return null;
441        }
442        return ComponentName.unflattenFromString(value);
443    }
444
445    @Nullable
446    static Intent parseIntentAttribute(XmlPullParser parser, String attribute) {
447        final String value = parseStringAttribute(parser, attribute);
448        if (TextUtils.isEmpty(value)) {
449            return null;
450        }
451        try {
452            return Intent.parseUri(value, /* flags =*/ 0);
453        } catch (URISyntaxException e) {
454            Slog.e(TAG, "Error parsing intent", e);
455            return null;
456        }
457    }
458
459    static void writeTagValue(XmlSerializer out, String tag, String value) throws IOException {
460        if (TextUtils.isEmpty(value)) return;
461
462        out.startTag(null, tag);
463        out.attribute(null, ATTR_VALUE, value);
464        out.endTag(null, tag);
465    }
466
467    static void writeTagValue(XmlSerializer out, String tag, long value) throws IOException {
468        writeTagValue(out, tag, Long.toString(value));
469    }
470
471    static void writeTagValue(XmlSerializer out, String tag, ComponentName name) throws IOException {
472        if (name == null) return;
473        writeTagValue(out, tag, name.flattenToString());
474    }
475
476    static void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle)
477            throws IOException, XmlPullParserException {
478        if (bundle == null) return;
479
480        out.startTag(null, tag);
481        bundle.saveToXml(out);
482        out.endTag(null, tag);
483    }
484
485    static void writeAttr(XmlSerializer out, String name, String value) throws IOException {
486        if (TextUtils.isEmpty(value)) return;
487
488        out.attribute(null, name, value);
489    }
490
491    static void writeAttr(XmlSerializer out, String name, long value) throws IOException {
492        writeAttr(out, name, String.valueOf(value));
493    }
494
495    static void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException {
496        if (comp == null) return;
497        writeAttr(out, name, comp.flattenToString());
498    }
499
500    static void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException {
501        if (intent == null) return;
502
503        writeAttr(out, name, intent.toUri(/* flags =*/ 0));
504    }
505
506    @VisibleForTesting
507    void saveBaseStateLocked() {
508        final AtomicFile file = getBaseStateFile();
509        if (DEBUG) {
510            Slog.d(TAG, "Saving to " + file.getBaseFile());
511        }
512
513        FileOutputStream outs = null;
514        try {
515            outs = file.startWrite();
516
517            // Write to XML
518            XmlSerializer out = new FastXmlSerializer();
519            out.setOutput(outs, StandardCharsets.UTF_8.name());
520            out.startDocument(null, true);
521            out.startTag(null, TAG_ROOT);
522
523            // Body.
524            writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime);
525
526            // Epilogue.
527            out.endTag(null, TAG_ROOT);
528            out.endDocument();
529
530            // Close.
531            file.finishWrite(outs);
532        } catch (IOException e) {
533            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
534            file.failWrite(outs);
535        }
536    }
537
538    private void loadBaseStateLocked() {
539        mRawLastResetTime = 0;
540
541        final AtomicFile file = getBaseStateFile();
542        if (DEBUG) {
543            Slog.d(TAG, "Loading from " + file.getBaseFile());
544        }
545        try (FileInputStream in = file.openRead()) {
546            XmlPullParser parser = Xml.newPullParser();
547            parser.setInput(in, StandardCharsets.UTF_8.name());
548
549            int type;
550            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
551                if (type != XmlPullParser.START_TAG) {
552                    continue;
553                }
554                final int depth = parser.getDepth();
555                // Check the root tag
556                final String tag = parser.getName();
557                if (depth == 1) {
558                    if (!TAG_ROOT.equals(tag)) {
559                        Slog.e(TAG, "Invalid root tag: " + tag);
560                        return;
561                    }
562                    continue;
563                }
564                // Assume depth == 2
565                switch (tag) {
566                    case TAG_LAST_RESET_TIME:
567                        mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE);
568                        break;
569                    default:
570                        Slog.e(TAG, "Invalid tag: " + tag);
571                        break;
572                }
573            }
574        } catch (FileNotFoundException e) {
575            // Use the default
576        } catch (IOException|XmlPullParserException e) {
577            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
578
579            mRawLastResetTime = 0;
580        }
581        // Adjust the last reset time.
582        getLastResetTimeLocked();
583    }
584
585    private void saveUserLocked(@UserIdInt int userId) {
586        final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
587        if (DEBUG) {
588            Slog.d(TAG, "Saving to " + path);
589        }
590        path.mkdirs();
591        final AtomicFile file = new AtomicFile(path);
592        FileOutputStream outs = null;
593        try {
594            outs = file.startWrite();
595
596            // Write to XML
597            XmlSerializer out = new FastXmlSerializer();
598            out.setOutput(outs, StandardCharsets.UTF_8.name());
599            out.startDocument(null, true);
600
601            getUserShortcutsLocked(userId).saveToXml(out);
602
603            out.endDocument();
604
605            // Close.
606            file.finishWrite(outs);
607        } catch (IOException|XmlPullParserException e) {
608            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
609            file.failWrite(outs);
610        }
611    }
612
613    static IOException throwForInvalidTag(int depth, String tag) throws IOException {
614        throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth));
615    }
616
617    @Nullable
618    private UserShortcuts loadUserLocked(@UserIdInt int userId) {
619        final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
620        if (DEBUG) {
621            Slog.d(TAG, "Loading from " + path);
622        }
623        final AtomicFile file = new AtomicFile(path);
624
625        final FileInputStream in;
626        try {
627            in = file.openRead();
628        } catch (FileNotFoundException e) {
629            if (DEBUG) {
630                Slog.d(TAG, "Not found " + path);
631            }
632            return null;
633        }
634        UserShortcuts ret = null;
635        try {
636            XmlPullParser parser = Xml.newPullParser();
637            parser.setInput(in, StandardCharsets.UTF_8.name());
638
639            int type;
640            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
641                if (type != XmlPullParser.START_TAG) {
642                    continue;
643                }
644                final int depth = parser.getDepth();
645
646                final String tag = parser.getName();
647                if (DEBUG_LOAD) {
648                    Slog.d(TAG, String.format("depth=%d type=%d name=%s",
649                            depth, type, tag));
650                }
651                if ((depth == 1) && UserShortcuts.TAG_ROOT.equals(tag)) {
652                    ret = UserShortcuts.loadFromXml(parser, userId);
653                    continue;
654                }
655                throwForInvalidTag(depth, tag);
656            }
657            return ret;
658        } catch (IOException|XmlPullParserException e) {
659            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
660            return null;
661        } finally {
662            IoUtils.closeQuietly(in);
663        }
664    }
665
666    private void scheduleSaveBaseState() {
667        scheduleSave(UserHandle.USER_NULL); // Special case -- use USER_NULL for base state.
668    }
669
670    void scheduleSaveUser(@UserIdInt int userId) {
671        scheduleSave(userId);
672    }
673
674    // In order to re-schedule, we need to reuse the same instance, so keep it in final.
675    private final Runnable mSaveDirtyInfoRunner = this::saveDirtyInfo;
676
677    private void scheduleSave(@UserIdInt int userId) {
678        if (DEBUG) {
679            Slog.d(TAG, "Scheduling to save for " + userId);
680        }
681        synchronized (mLock) {
682            if (!mDirtyUserIds.contains(userId)) {
683                mDirtyUserIds.add(userId);
684            }
685        }
686        // If already scheduled, remove that and re-schedule in N seconds.
687        mHandler.removeCallbacks(mSaveDirtyInfoRunner);
688        mHandler.postDelayed(mSaveDirtyInfoRunner, mSaveDelayMillis);
689    }
690
691    @VisibleForTesting
692    void saveDirtyInfo() {
693        if (DEBUG) {
694            Slog.d(TAG, "saveDirtyInfo");
695        }
696        synchronized (mLock) {
697            for (int i = mDirtyUserIds.size() - 1; i >= 0; i--) {
698                final int userId = mDirtyUserIds.get(i);
699                if (userId == UserHandle.USER_NULL) { // USER_NULL for base state.
700                    saveBaseStateLocked();
701                } else {
702                    saveUserLocked(userId);
703                }
704            }
705            mDirtyUserIds.clear();
706        }
707    }
708
709    /** Return the last reset time. */
710    long getLastResetTimeLocked() {
711        updateTimesLocked();
712        return mRawLastResetTime;
713    }
714
715    /** Return the next reset time. */
716    long getNextResetTimeLocked() {
717        updateTimesLocked();
718        return mRawLastResetTime + mResetInterval;
719    }
720
721    static boolean isClockValid(long time) {
722        return time >= 1420070400; // Thu, 01 Jan 2015 00:00:00 GMT
723    }
724
725    /**
726     * Update the last reset time.
727     */
728    private void updateTimesLocked() {
729
730        final long now = injectCurrentTimeMillis();
731
732        final long prevLastResetTime = mRawLastResetTime;
733
734        if (mRawLastResetTime == 0) { // first launch.
735            // TODO Randomize??
736            mRawLastResetTime = now;
737        } else if (now < mRawLastResetTime) {
738            // Clock rewound.
739            if (isClockValid(now)) {
740                Slog.w(TAG, "Clock rewound");
741                // TODO Randomize??
742                mRawLastResetTime = now;
743            }
744        } else {
745            if ((mRawLastResetTime + mResetInterval) <= now) {
746                final long offset = mRawLastResetTime % mResetInterval;
747                mRawLastResetTime = ((now / mResetInterval) * mResetInterval) + offset;
748            }
749        }
750        if (prevLastResetTime != mRawLastResetTime) {
751            scheduleSaveBaseState();
752        }
753    }
754
755    /** Return the per-user state. */
756    @GuardedBy("mLock")
757    @NonNull
758    UserShortcuts getUserShortcutsLocked(@UserIdInt int userId) {
759        UserShortcuts userPackages = mUsers.get(userId);
760        if (userPackages == null) {
761            userPackages = loadUserLocked(userId);
762            if (userPackages == null) {
763                userPackages = new UserShortcuts(userId);
764            }
765            mUsers.put(userId, userPackages);
766        }
767        return userPackages;
768    }
769
770    /** Return the per-user per-package state. */
771    @GuardedBy("mLock")
772    @NonNull
773    PackageShortcuts getPackageShortcutsLocked(
774            @NonNull String packageName, @UserIdInt int userId) {
775        return getUserShortcutsLocked(userId).getPackageShortcuts(packageName);
776    }
777
778    @GuardedBy("mLock")
779    @NonNull
780    LauncherShortcuts getLauncherShortcuts(
781            @NonNull String packageName, @UserIdInt int userId) {
782        return getUserShortcutsLocked(userId).getLauncherShortcuts(packageName);
783    }
784
785    // === Caller validation ===
786
787    void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
788        if (shortcut.getBitmapPath() != null) {
789            if (DEBUG) {
790                Slog.d(TAG, "Removing " + shortcut.getBitmapPath());
791            }
792            new File(shortcut.getBitmapPath()).delete();
793
794            shortcut.setBitmapPath(null);
795            shortcut.setIconResourceId(0);
796            shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
797        }
798    }
799
800    @VisibleForTesting
801    static class FileOutputStreamWithPath extends FileOutputStream {
802        private final File mFile;
803
804        public FileOutputStreamWithPath(File file) throws FileNotFoundException {
805            super(file);
806            mFile = file;
807        }
808
809        public File getFile() {
810            return mFile;
811        }
812    }
813
814    /**
815     * Build the cached bitmap filename for a shortcut icon.
816     *
817     * The filename will be based on the ID, except certain characters will be escaped.
818     */
819    @VisibleForTesting
820    FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
821            throws IOException {
822        final File packagePath = new File(getUserBitmapFilePath(userId),
823                shortcut.getPackageName());
824        if (!packagePath.isDirectory()) {
825            packagePath.mkdirs();
826            if (!packagePath.isDirectory()) {
827                throw new IOException("Unable to create directory " + packagePath);
828            }
829            SELinux.restorecon(packagePath);
830        }
831
832        final String baseName = String.valueOf(injectCurrentTimeMillis());
833        for (int suffix = 0;; suffix++) {
834            final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
835            final File file = new File(packagePath, filename);
836            if (!file.exists()) {
837                if (DEBUG) {
838                    Slog.d(TAG, "Saving icon to " + file.getAbsolutePath());
839                }
840                return new FileOutputStreamWithPath(file);
841            }
842        }
843    }
844
845    void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
846        if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
847            return;
848        }
849
850        final long token = injectClearCallingIdentity();
851        try {
852            // Clear icon info on the shortcut.
853            shortcut.setIconResourceId(0);
854            shortcut.setBitmapPath(null);
855
856            final Icon icon = shortcut.getIcon();
857            if (icon == null) {
858                return; // has no icon
859            }
860
861            Bitmap bitmap = null;
862            try {
863                switch (icon.getType()) {
864                    case Icon.TYPE_RESOURCE: {
865                        injectValidateIconResPackage(shortcut, icon);
866
867                        shortcut.setIconResourceId(icon.getResId());
868                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
869                        return;
870                    }
871                    case Icon.TYPE_BITMAP: {
872                        bitmap = icon.getBitmap();
873                        break;
874                    }
875                    case Icon.TYPE_URI: {
876                        final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId);
877
878                        try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
879
880                            bitmap = BitmapFactory.decodeStream(is);
881
882                        } catch (IOException e) {
883                            Slog.e(TAG, "Unable to load icon from " + uri);
884                            return;
885                        }
886                        break;
887                    }
888                    default:
889                        // This shouldn't happen because we've already validated the icon, but
890                        // just in case.
891                        throw ShortcutInfo.getInvalidIconException();
892                }
893                if (bitmap == null) {
894                    Slog.e(TAG, "Null bitmap detected");
895                    return;
896                }
897                // Shrink and write to the file.
898                File path = null;
899                try {
900                    final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
901                    try {
902                        path = out.getFile();
903
904                        shrinkBitmap(bitmap, mMaxIconDimension)
905                                .compress(mIconPersistFormat, mIconPersistQuality, out);
906
907                        shortcut.setBitmapPath(out.getFile().getAbsolutePath());
908                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
909                    } finally {
910                        IoUtils.closeQuietly(out);
911                    }
912                } catch (IOException|RuntimeException e) {
913                    // STOPSHIP Change wtf to e
914                    Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
915                    if (path != null && path.exists()) {
916                        path.delete();
917                    }
918                }
919            } finally {
920                if (bitmap != null) {
921                    bitmap.recycle();
922                }
923                // Once saved, we won't use the original icon information, so null it out.
924                shortcut.clearIcon();
925            }
926        } finally {
927            injectRestoreCallingIdentity(token);
928        }
929    }
930
931    // Unfortunately we can't do this check in unit tests because we fake creator package names,
932    // so override in unit tests.
933    // TODO CTS this case.
934    void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
935        if (!shortcut.getPackageName().equals(icon.getResPackage())) {
936            throw new IllegalArgumentException(
937                    "Icon resource must reside in shortcut owner package");
938        }
939    }
940
941    @VisibleForTesting
942    static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
943        // Original width/height.
944        final int ow = in.getWidth();
945        final int oh = in.getHeight();
946        if ((ow <= maxSize) && (oh <= maxSize)) {
947            if (DEBUG) {
948                Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh));
949            }
950            return in;
951        }
952        final int longerDimension = Math.max(ow, oh);
953
954        // New width and height.
955        final int nw = ow * maxSize / longerDimension;
956        final int nh = oh * maxSize / longerDimension;
957        if (DEBUG) {
958            Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d",
959                    ow, oh, nw, nh));
960        }
961
962        final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888);
963        final Canvas c = new Canvas(scaledBitmap);
964
965        final RectF dst = new RectF(0, 0, nw, nh);
966
967        c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null);
968
969        in.recycle();
970
971        return scaledBitmap;
972    }
973
974    // === Caller validation ===
975
976    private boolean isCallerSystem() {
977        final int callingUid = injectBinderCallingUid();
978         return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
979    }
980
981    private boolean isCallerShell() {
982        final int callingUid = injectBinderCallingUid();
983        return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
984    }
985
986    private void enforceSystemOrShell() {
987        Preconditions.checkState(isCallerSystem() || isCallerShell(),
988                "Caller must be system or shell");
989    }
990
991    private void enforceShell() {
992        Preconditions.checkState(isCallerShell(), "Caller must be shell");
993    }
994
995    private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) {
996        Preconditions.checkStringNotEmpty(packageName, "packageName");
997
998        if (isCallerSystem()) {
999            return; // no check
1000        }
1001
1002        final int callingUid = injectBinderCallingUid();
1003
1004        // Otherwise, make sure the arguments are valid.
1005        if (UserHandle.getUserId(callingUid) != userId) {
1006            throw new SecurityException("Invalid user-ID");
1007        }
1008        if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
1009            return; // Caller is valid.
1010        }
1011        throw new SecurityException("Caller UID= doesn't own " + packageName);
1012    }
1013
1014    // Test overrides it.
1015    int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
1016        try {
1017
1018            // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info?
1019
1020            return mContext.getPackageManager().getPackageUidAsUser(packageName,
1021                    PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE
1022                            | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
1023        } catch (NameNotFoundException e) {
1024            return -1;
1025        }
1026    }
1027
1028    void postToHandler(Runnable r) {
1029        mHandler.post(r);
1030    }
1031
1032    /**
1033     * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
1034     */
1035    void enforceMaxDynamicShortcuts(int numShortcuts) {
1036        if (numShortcuts > mMaxDynamicShortcuts) {
1037            throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
1038        }
1039    }
1040
1041    /**
1042     * - Sends a notification to LauncherApps
1043     * - Write to file
1044     */
1045    private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) {
1046        notifyListeners(packageName, userId);
1047        scheduleSaveUser(userId);
1048    }
1049
1050    private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) {
1051        postToHandler(() -> {
1052            final ArrayList<ShortcutChangeListener> copy;
1053            synchronized (mLock) {
1054                copy = new ArrayList<>(mListeners);
1055            }
1056            // Note onShortcutChanged() needs to be called with the system service permissions.
1057            for (int i = copy.size() - 1; i >= 0; i--) {
1058                copy.get(i).onShortcutChanged(packageName, userId);
1059            }
1060        });
1061    }
1062
1063    /**
1064     * Clean up / validate an incoming shortcut.
1065     * - Make sure all mandatory fields are set.
1066     * - Make sure the intent's extras are persistable, and them to set
1067     *  {@link ShortcutInfo#mIntentPersistableExtras}.  Also clear its extras.
1068     * - Clear flags.
1069     *
1070     * TODO Detailed unit tests
1071     */
1072    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
1073        Preconditions.checkNotNull(shortcut, "Null shortcut detected");
1074        if (shortcut.getActivityComponent() != null) {
1075            Preconditions.checkState(
1076                    shortcut.getPackageName().equals(
1077                            shortcut.getActivityComponent().getPackageName()),
1078                    "Activity package name mismatch");
1079        }
1080
1081        if (!forUpdate) {
1082            shortcut.enforceMandatoryFields();
1083        }
1084        if (shortcut.getIcon() != null) {
1085            ShortcutInfo.validateIcon(shortcut.getIcon());
1086        }
1087
1088        validateForXml(shortcut.getId());
1089        validateForXml(shortcut.getTitle());
1090        validatePersistableBundleForXml(shortcut.getIntentPersistableExtras());
1091        validatePersistableBundleForXml(shortcut.getExtras());
1092
1093        shortcut.replaceFlags(0);
1094    }
1095
1096    // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those
1097    // characters.
1098
1099    private static void validatePersistableBundleForXml(PersistableBundle b) {
1100        if (b == null || b.size() == 0) {
1101            return;
1102        }
1103        for (String key : b.keySet()) {
1104            validateForXml(key);
1105            final Object value = b.get(key);
1106            if (value == null) {
1107                continue;
1108            } else if (value instanceof String) {
1109                validateForXml((String) value);
1110            } else if (value instanceof String[]) {
1111                for (String v : (String[]) value) {
1112                    validateForXml(v);
1113                }
1114            } else if (value instanceof PersistableBundle) {
1115                validatePersistableBundleForXml((PersistableBundle) value);
1116            }
1117        }
1118    }
1119
1120    private static void validateForXml(String s) {
1121        if (TextUtils.isEmpty(s)) {
1122            return;
1123        }
1124        for (int i = s.length() - 1; i >= 0; i--) {
1125            if (!isAllowedInXml(s.charAt(i))) {
1126                throw new IllegalArgumentException("Unsupported character detected in: " + s);
1127            }
1128        }
1129    }
1130
1131    private static boolean isAllowedInXml(char c) {
1132        return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
1133    }
1134
1135    // === APIs ===
1136
1137    @Override
1138    public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1139            @UserIdInt int userId) {
1140        verifyCaller(packageName, userId);
1141
1142        final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1143        final int size = newShortcuts.size();
1144
1145        synchronized (mLock) {
1146            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1147
1148            // Throttling.
1149            if (!ps.tryApiCall(this)) {
1150                return false;
1151            }
1152            enforceMaxDynamicShortcuts(size);
1153
1154            // Validate the shortcuts.
1155            for (int i = 0; i < size; i++) {
1156                fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
1157            }
1158
1159            // First, remove all un-pinned; dynamic shortcuts
1160            ps.deleteAllDynamicShortcuts(this);
1161
1162            // Then, add/update all.  We need to make sure to take over "pinned" flag.
1163            for (int i = 0; i < size; i++) {
1164                final ShortcutInfo newShortcut = newShortcuts.get(i);
1165                ps.addDynamicShortcut(this, newShortcut);
1166            }
1167        }
1168        userPackageChanged(packageName, userId);
1169        return true;
1170    }
1171
1172    @Override
1173    public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1174            @UserIdInt int userId) {
1175        verifyCaller(packageName, userId);
1176
1177        final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1178        final int size = newShortcuts.size();
1179
1180        synchronized (mLock) {
1181            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1182
1183            // Throttling.
1184            if (!ps.tryApiCall(this)) {
1185                return false;
1186            }
1187
1188            for (int i = 0; i < size; i++) {
1189                final ShortcutInfo source = newShortcuts.get(i);
1190                fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
1191
1192                final ShortcutInfo target = ps.findShortcutById(source.getId());
1193                if (target != null) {
1194                    final boolean replacingIcon = (source.getIcon() != null);
1195                    if (replacingIcon) {
1196                        removeIcon(userId, target);
1197                    }
1198
1199                    target.copyNonNullFieldsFrom(source);
1200
1201                    if (replacingIcon) {
1202                        saveIconAndFixUpShortcut(userId, target);
1203                    }
1204                }
1205            }
1206        }
1207        userPackageChanged(packageName, userId);
1208
1209        return true;
1210    }
1211
1212    @Override
1213    public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut,
1214            @UserIdInt int userId) {
1215        verifyCaller(packageName, userId);
1216
1217        synchronized (mLock) {
1218            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1219
1220            // Throttling.
1221            if (!ps.tryApiCall(this)) {
1222                return false;
1223            }
1224
1225            // Validate the shortcut.
1226            fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
1227
1228            // Add it.
1229            ps.addDynamicShortcut(this, newShortcut);
1230        }
1231        userPackageChanged(packageName, userId);
1232
1233        return true;
1234    }
1235
1236    @Override
1237    public void deleteDynamicShortcut(String packageName, String shortcutId,
1238            @UserIdInt int userId) {
1239        verifyCaller(packageName, userId);
1240        Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
1241
1242        synchronized (mLock) {
1243            getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
1244        }
1245        userPackageChanged(packageName, userId);
1246    }
1247
1248    @Override
1249    public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) {
1250        verifyCaller(packageName, userId);
1251
1252        synchronized (mLock) {
1253            getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
1254        }
1255        userPackageChanged(packageName, userId);
1256    }
1257
1258    @Override
1259    public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName,
1260            @UserIdInt int userId) {
1261        verifyCaller(packageName, userId);
1262        synchronized (mLock) {
1263            return getShortcutsWithQueryLocked(
1264                    packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1265                    ShortcutInfo::isDynamic);
1266        }
1267    }
1268
1269    @Override
1270    public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName,
1271            @UserIdInt int userId) {
1272        verifyCaller(packageName, userId);
1273        synchronized (mLock) {
1274            return getShortcutsWithQueryLocked(
1275                    packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1276                    ShortcutInfo::isPinned);
1277        }
1278    }
1279
1280    private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName,
1281            @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) {
1282
1283        final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1284
1285        getPackageShortcutsLocked(packageName, userId).findAll(this, ret, query, cloneFlags,
1286                /* callingLauncher= */ null);
1287
1288        return new ParceledListSlice<>(ret);
1289    }
1290
1291    @Override
1292    public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId)
1293            throws RemoteException {
1294        verifyCaller(packageName, userId);
1295
1296        return mMaxDynamicShortcuts;
1297    }
1298
1299    @Override
1300    public int getRemainingCallCount(String packageName, @UserIdInt int userId) {
1301        verifyCaller(packageName, userId);
1302
1303        synchronized (mLock) {
1304            return mMaxDailyUpdates
1305                    - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
1306        }
1307    }
1308
1309    @Override
1310    public long getRateLimitResetTime(String packageName, @UserIdInt int userId) {
1311        verifyCaller(packageName, userId);
1312
1313        synchronized (mLock) {
1314            return getNextResetTimeLocked();
1315        }
1316    }
1317
1318    @Override
1319    public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
1320        synchronized (mLock) {
1321            return mMaxIconDimension;
1322        }
1323    }
1324
1325    /**
1326     * Reset all throttling, for developer options and command line.  Only system/shell can call it.
1327     */
1328    @Override
1329    public void resetThrottling() {
1330        enforceSystemOrShell();
1331
1332        resetThrottlingInner(getCallingUserId());
1333    }
1334
1335    void resetThrottlingInner(@UserIdInt int userId) {
1336        synchronized (mLock) {
1337            getUserShortcutsLocked(userId).resetThrottling();
1338        }
1339        scheduleSaveUser(userId);
1340        Slog.i(TAG, "ShortcutManager: throttling counter reset");
1341    }
1342
1343    // We override this method in unit tests to do a simpler check.
1344    boolean hasShortcutHostPermission(@NonNull String callingPackage, int userId) {
1345        return hasShortcutHostPermissionInner(callingPackage, userId);
1346    }
1347
1348    // This method is extracted so we can directly call this method from unit tests,
1349    // even when hasShortcutPermission() is overridden.
1350    @VisibleForTesting
1351    boolean hasShortcutHostPermissionInner(@NonNull String callingPackage, int userId) {
1352        synchronized (mLock) {
1353            long start = 0;
1354            if (DEBUG) {
1355                start = System.currentTimeMillis();
1356            }
1357
1358            final UserShortcuts user = getUserShortcutsLocked(userId);
1359
1360            final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
1361
1362            // Default launcher from package manager.
1363            final ComponentName defaultLauncher = injectPackageManagerInternal()
1364                    .getHomeActivitiesAsUser(allHomeCandidates, userId);
1365
1366            ComponentName detected;
1367            if (defaultLauncher != null) {
1368                detected = defaultLauncher;
1369                if (DEBUG) {
1370                    Slog.v(TAG, "Default launcher from PM: " + detected);
1371                }
1372            } else {
1373                detected = user.getLauncherComponent();
1374
1375                // TODO: Make sure it's still enabled.
1376                if (DEBUG) {
1377                    Slog.v(TAG, "Cached launcher: " + detected);
1378                }
1379            }
1380
1381            if (detected == null) {
1382                // If we reach here, that means it's the first check since the user was created,
1383                // and there's already multiple launchers and there's no default set.
1384                // Find the system one with the highest priority.
1385                // (We need to check the priority too because of FallbackHome in Settings.)
1386                // If there's no system launcher yet, then no one can access shortcuts, until
1387                // the user explicitly
1388                final int size = allHomeCandidates.size();
1389
1390                int lastPriority = Integer.MIN_VALUE;
1391                for (int i = 0; i < size; i++) {
1392                    final ResolveInfo ri = allHomeCandidates.get(i);
1393                    if (!ri.activityInfo.applicationInfo.isSystemApp()) {
1394                        continue;
1395                    }
1396                    if (DEBUG) {
1397                        Slog.d(TAG, String.format("hasShortcutPermissionInner: pkg=%s prio=%d",
1398                                ri.activityInfo.getComponentName(), ri.priority));
1399                    }
1400                    if (ri.priority < lastPriority) {
1401                        continue;
1402                    }
1403                    detected = ri.activityInfo.getComponentName();
1404                    lastPriority = ri.priority;
1405                }
1406            }
1407            if (DEBUG) {
1408                long end = System.currentTimeMillis();
1409                Slog.v(TAG, String.format("hasShortcutPermission took %d ms", end - start));
1410            }
1411            if (detected != null) {
1412                if (DEBUG) {
1413                    Slog.v(TAG, "Detected launcher: " + detected);
1414                }
1415                user.setLauncherComponent(this, detected);
1416                return detected.getPackageName().equals(callingPackage);
1417            } else {
1418                // Default launcher not found.
1419                return false;
1420            }
1421        }
1422    }
1423
1424    /**
1425     * Entry point from {@link LauncherApps}.
1426     */
1427    private class LocalService extends ShortcutServiceInternal {
1428        @Override
1429        public List<ShortcutInfo> getShortcuts(
1430                @NonNull String callingPackage, long changedSince,
1431                @Nullable String packageName, @Nullable ComponentName componentName,
1432                int queryFlags, int userId) {
1433            final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1434            final int cloneFlag =
1435                    ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0)
1436                            ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER
1437                            : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO;
1438
1439            synchronized (mLock) {
1440                if (packageName != null) {
1441                    getShortcutsInnerLocked(
1442                            callingPackage, packageName, changedSince,
1443                            componentName, queryFlags, userId, ret, cloneFlag);
1444                } else {
1445                    final ArrayMap<String, PackageShortcuts> packages =
1446                            getUserShortcutsLocked(userId).getPackages();
1447                    for (int i = packages.size() - 1; i >= 0; i--) {
1448                        getShortcutsInnerLocked(
1449                                callingPackage, packages.keyAt(i), changedSince,
1450                                componentName, queryFlags, userId, ret, cloneFlag);
1451                    }
1452                }
1453            }
1454            return ret;
1455        }
1456
1457        private void getShortcutsInnerLocked(@NonNull String callingPackage,
1458                @Nullable String packageName,long changedSince,
1459                @Nullable ComponentName componentName, int queryFlags,
1460                int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) {
1461            getPackageShortcutsLocked(packageName, userId).findAll(ShortcutService.this, ret,
1462                    (ShortcutInfo si) -> {
1463                        if (si.getLastChangedTimestamp() < changedSince) {
1464                            return false;
1465                        }
1466                        if (componentName != null
1467                                && !componentName.equals(si.getActivityComponent())) {
1468                            return false;
1469                        }
1470                        final boolean matchDynamic =
1471                                ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0)
1472                                        && si.isDynamic();
1473                        final boolean matchPinned =
1474                                ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0)
1475                                        && si.isPinned();
1476                        return matchDynamic || matchPinned;
1477                    }, cloneFlag, callingPackage);
1478        }
1479
1480        @Override
1481        public List<ShortcutInfo> getShortcutInfo(
1482                @NonNull String callingPackage,
1483                @NonNull String packageName, @Nullable List<String> ids, int userId) {
1484            // Calling permission must be checked by LauncherAppsImpl.
1485            Preconditions.checkStringNotEmpty(packageName, "packageName");
1486
1487            final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size());
1488            final ArraySet<String> idSet = new ArraySet<>(ids);
1489            synchronized (mLock) {
1490                getPackageShortcutsLocked(packageName, userId).findAll(
1491                        ShortcutService.this, ret,
1492                        (ShortcutInfo si) -> idSet.contains(si.getId()),
1493                        ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER, callingPackage);
1494            }
1495            return ret;
1496        }
1497
1498        @Override
1499        public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName,
1500                @NonNull List<String> shortcutIds, int userId) {
1501            // Calling permission must be checked by LauncherAppsImpl.
1502            Preconditions.checkStringNotEmpty(packageName, "packageName");
1503            Preconditions.checkNotNull(shortcutIds, "shortcutIds");
1504
1505            synchronized (mLock) {
1506                getLauncherShortcuts(callingPackage, userId).pinShortcuts(
1507                        ShortcutService.this, packageName, shortcutIds);
1508            }
1509            userPackageChanged(packageName, userId);
1510        }
1511
1512        @Override
1513        public Intent createShortcutIntent(@NonNull String callingPackage,
1514                @NonNull String packageName, @NonNull String shortcutId, int userId) {
1515            // Calling permission must be checked by LauncherAppsImpl.
1516            Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty");
1517            Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty");
1518
1519            synchronized (mLock) {
1520                final ShortcutInfo fullShortcut =
1521                        getPackageShortcutsLocked(packageName, userId)
1522                        .findShortcutById(shortcutId);
1523                return fullShortcut == null ? null : fullShortcut.getIntent();
1524            }
1525        }
1526
1527        @Override
1528        public void addListener(@NonNull ShortcutChangeListener listener) {
1529            synchronized (mLock) {
1530                mListeners.add(Preconditions.checkNotNull(listener));
1531            }
1532        }
1533
1534        @Override
1535        public int getShortcutIconResId(@NonNull String callingPackage,
1536                @NonNull ShortcutInfo shortcut, int userId) {
1537            Preconditions.checkNotNull(shortcut, "shortcut");
1538
1539            synchronized (mLock) {
1540                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1541                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1542                return (shortcutInfo != null && shortcutInfo.hasIconResource())
1543                        ? shortcutInfo.getIconResourceId() : 0;
1544            }
1545        }
1546
1547        @Override
1548        public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
1549                @NonNull ShortcutInfo shortcutIn, int userId) {
1550            Preconditions.checkNotNull(shortcutIn, "shortcut");
1551
1552            synchronized (mLock) {
1553                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1554                        shortcutIn.getPackageName(), userId).findShortcutById(shortcutIn.getId());
1555                if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
1556                    return null;
1557                }
1558                try {
1559                    if (shortcutInfo.getBitmapPath() == null) {
1560                        Slog.w(TAG, "null bitmap detected in getShortcutIconFd()");
1561                        return null;
1562                    }
1563                    return ParcelFileDescriptor.open(
1564                            new File(shortcutInfo.getBitmapPath()),
1565                            ParcelFileDescriptor.MODE_READ_ONLY);
1566                } catch (FileNotFoundException e) {
1567                    Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
1568                    return null;
1569                }
1570            }
1571        }
1572
1573        @Override
1574        public boolean hasShortcutHostPermission(@NonNull String callingPackage, int userId) {
1575            return ShortcutService.this.hasShortcutHostPermission(callingPackage, userId);
1576        }
1577    }
1578
1579    // === Dump ===
1580
1581    @Override
1582    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1583        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
1584                != PackageManager.PERMISSION_GRANTED) {
1585            pw.println("Permission Denial: can't dump UserManager from from pid="
1586                    + Binder.getCallingPid()
1587                    + ", uid=" + Binder.getCallingUid()
1588                    + " without permission "
1589                    + android.Manifest.permission.DUMP);
1590            return;
1591        }
1592        dumpInner(pw);
1593    }
1594
1595    @VisibleForTesting
1596    void dumpInner(PrintWriter pw) {
1597        synchronized (mLock) {
1598            final long now = injectCurrentTimeMillis();
1599            pw.print("Now: [");
1600            pw.print(now);
1601            pw.print("] ");
1602            pw.print(formatTime(now));
1603
1604            pw.print("  Raw last reset: [");
1605            pw.print(mRawLastResetTime);
1606            pw.print("] ");
1607            pw.print(formatTime(mRawLastResetTime));
1608
1609            final long last = getLastResetTimeLocked();
1610            pw.print("  Last reset: [");
1611            pw.print(last);
1612            pw.print("] ");
1613            pw.print(formatTime(last));
1614
1615            final long next = getNextResetTimeLocked();
1616            pw.print("  Next reset: [");
1617            pw.print(next);
1618            pw.print("] ");
1619            pw.print(formatTime(next));
1620            pw.println();
1621
1622            pw.print("  Max icon dim: ");
1623            pw.print(mMaxIconDimension);
1624            pw.print("  Icon format: ");
1625            pw.print(mIconPersistFormat);
1626            pw.print("  Icon quality: ");
1627            pw.print(mIconPersistQuality);
1628            pw.println();
1629
1630
1631            for (int i = 0; i < mUsers.size(); i++) {
1632                pw.println();
1633                mUsers.valueAt(i).dump(this, pw, "  ");
1634            }
1635        }
1636    }
1637
1638    static String formatTime(long time) {
1639        Time tobj = new Time();
1640        tobj.set(time);
1641        return tobj.format("%Y-%m-%d %H:%M:%S");
1642    }
1643
1644    // === Shell support ===
1645
1646    @Override
1647    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
1648            String[] args, ResultReceiver resultReceiver) throws RemoteException {
1649
1650        enforceShell();
1651
1652        (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
1653    }
1654
1655    static class CommandException extends Exception {
1656        public CommandException(String message) {
1657            super(message);
1658        }
1659    }
1660
1661    /**
1662     * Handle "adb shell cmd".
1663     */
1664    private class MyShellCommand extends ShellCommand {
1665
1666        private int mUserId = UserHandle.USER_SYSTEM;
1667
1668        private void parseOptions(boolean takeUser)
1669                throws CommandException {
1670            String opt;
1671            while ((opt = getNextOption()) != null) {
1672                switch (opt) {
1673                    case "--user":
1674                        if (takeUser) {
1675                            mUserId = UserHandle.parseUserArg(getNextArgRequired());
1676                            break;
1677                        }
1678                        // fallthrough
1679                    default:
1680                        throw new CommandException("Unknown option: " + opt);
1681                }
1682            }
1683        }
1684
1685        @Override
1686        public int onCommand(String cmd) {
1687            if (cmd == null) {
1688                return handleDefaultCommands(cmd);
1689            }
1690            final PrintWriter pw = getOutPrintWriter();
1691            try {
1692                switch (cmd) {
1693                    case "reset-package-throttling":
1694                        handleResetPackageThrottling();
1695                        break;
1696                    case "reset-throttling":
1697                        handleResetThrottling();
1698                        break;
1699                    case "override-config":
1700                        handleOverrideConfig();
1701                        break;
1702                    case "reset-config":
1703                        handleResetConfig();
1704                        break;
1705                    case "clear-default-launcher":
1706                        handleClearDefaultLauncher();
1707                        break;
1708                    case "get-default-launcher":
1709                        handleGetDefaultLauncher();
1710                        break;
1711                    case "refresh-default-launcher":
1712                        handleRefreshDefaultLauncher();
1713                        break;
1714                    default:
1715                        return handleDefaultCommands(cmd);
1716                }
1717            } catch (CommandException e) {
1718                pw.println("Error: " + e.getMessage());
1719                return 1;
1720            }
1721            pw.println("Success");
1722            return 0;
1723        }
1724
1725        @Override
1726        public void onHelp() {
1727            final PrintWriter pw = getOutPrintWriter();
1728            pw.println("Usage: cmd shortcut COMMAND [options ...]");
1729            pw.println();
1730            pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE");
1731            pw.println("    Reset throttling for a package");
1732            pw.println();
1733            pw.println("cmd shortcut reset-throttling");
1734            pw.println("    Reset throttling for all packages and users");
1735            pw.println();
1736            pw.println("cmd shortcut override-config CONFIG");
1737            pw.println("    Override the configuration for testing (will last until reboot)");
1738            pw.println();
1739            pw.println("cmd shortcut reset-config");
1740            pw.println("    Reset the configuration set with \"update-config\"");
1741            pw.println();
1742            pw.println("cmd shortcut clear-default-launcher [--user USER_ID]");
1743            pw.println("    Clear the cached default launcher");
1744            pw.println();
1745            pw.println("cmd shortcut get-default-launcher [--user USER_ID]");
1746            pw.println("    Show the cached default launcher");
1747            pw.println();
1748            pw.println("cmd shortcut refresh-default-launcher [--user USER_ID]");
1749            pw.println("    Refresh the cached default launcher");
1750            pw.println();
1751        }
1752
1753        private int handleResetThrottling() throws CommandException {
1754            parseOptions(/* takeUser =*/ true);
1755
1756            resetThrottlingInner(mUserId);
1757            return 0;
1758        }
1759
1760        private void handleResetPackageThrottling() throws CommandException {
1761            parseOptions(/* takeUser =*/ true);
1762
1763            final String packageName = getNextArgRequired();
1764
1765            synchronized (mLock) {
1766                getPackageShortcutsLocked(packageName, mUserId).resetRateLimitingForCommandLine();
1767                saveUserLocked(mUserId);
1768            }
1769        }
1770
1771        private void handleOverrideConfig() throws CommandException {
1772            final String config = getNextArgRequired();
1773
1774            synchronized (mLock) {
1775                if (!updateConfigurationLocked(config)) {
1776                    throw new CommandException("override-config failed.  See logcat for details.");
1777                }
1778            }
1779        }
1780
1781        private void handleResetConfig() {
1782            synchronized (mLock) {
1783                loadConfigurationLocked();
1784            }
1785        }
1786
1787        private void clearLauncher() {
1788            synchronized (mLock) {
1789                getUserShortcutsLocked(mUserId).setLauncherComponent(
1790                        ShortcutService.this, null);
1791            }
1792        }
1793
1794        private void showLauncher() {
1795            synchronized (mLock) {
1796                // This ensures to set the cached launcher.  Package name doesn't matter.
1797                hasShortcutHostPermissionInner("-", mUserId);
1798
1799                getOutPrintWriter().println("Launcher: "
1800                        + getUserShortcutsLocked(mUserId).getLauncherComponent());
1801            }
1802        }
1803
1804        private void handleClearDefaultLauncher() throws CommandException {
1805            parseOptions(/* takeUser =*/ true);
1806
1807            clearLauncher();
1808        }
1809
1810        private void handleGetDefaultLauncher() throws CommandException {
1811            parseOptions(/* takeUser =*/ true);
1812
1813            showLauncher();
1814        }
1815
1816        private void handleRefreshDefaultLauncher() throws CommandException {
1817            parseOptions(/* takeUser =*/ true);
1818
1819            clearLauncher();
1820            showLauncher();
1821        }
1822    }
1823
1824    // === Unit test support ===
1825
1826    // Injection point.
1827    long injectCurrentTimeMillis() {
1828        return System.currentTimeMillis();
1829    }
1830
1831    // Injection point.
1832    int injectBinderCallingUid() {
1833        return getCallingUid();
1834    }
1835
1836    final int getCallingUserId() {
1837        return UserHandle.getUserId(injectBinderCallingUid());
1838    }
1839
1840    // Injection point.
1841    long injectClearCallingIdentity() {
1842        return Binder.clearCallingIdentity();
1843    }
1844
1845    // Injection point.
1846    void injectRestoreCallingIdentity(long token) {
1847        Binder.restoreCallingIdentity(token);
1848    }
1849
1850    final void wtf(String message) {
1851        Slog.wtf(TAG, message, /* exception= */ null);
1852    }
1853
1854    void wtf(String message, Exception e) {
1855        Slog.wtf(TAG, message, e);
1856    }
1857
1858    File injectSystemDataPath() {
1859        return Environment.getDataSystemDirectory();
1860    }
1861
1862    File injectUserDataPath(@UserIdInt int userId) {
1863        return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
1864    }
1865
1866    @VisibleForTesting
1867    boolean injectIsLowRamDevice() {
1868        return ActivityManager.isLowRamDeviceStatic();
1869    }
1870
1871    PackageManagerInternal injectPackageManagerInternal() {
1872        return mPackageManagerInternal;
1873    }
1874
1875    File getUserBitmapFilePath(@UserIdInt int userId) {
1876        return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
1877    }
1878
1879    @VisibleForTesting
1880    SparseArray<UserShortcuts> getShortcutsForTest() {
1881        return mUsers;
1882    }
1883
1884    @VisibleForTesting
1885    int getMaxDynamicShortcutsForTest() {
1886        return mMaxDynamicShortcuts;
1887    }
1888
1889    @VisibleForTesting
1890    int getMaxDailyUpdatesForTest() {
1891        return mMaxDailyUpdates;
1892    }
1893
1894    @VisibleForTesting
1895    long getResetIntervalForTest() {
1896        return mResetInterval;
1897    }
1898
1899    @VisibleForTesting
1900    int getMaxIconDimensionForTest() {
1901        return mMaxIconDimension;
1902    }
1903
1904    @VisibleForTesting
1905    CompressFormat getIconPersistFormatForTest() {
1906        return mIconPersistFormat;
1907    }
1908
1909    @VisibleForTesting
1910    int getIconPersistQualityForTest() {
1911        return mIconPersistQuality;
1912    }
1913
1914    @VisibleForTesting
1915    ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) {
1916        synchronized (mLock) {
1917            return getPackageShortcutsLocked(packageName, userId).findShortcutById(shortcutId);
1918        }
1919    }
1920}
1921
1922/**
1923 * Per-user information.
1924 */
1925class UserShortcuts {
1926    private static final String TAG = ShortcutService.TAG;
1927
1928    static final String TAG_ROOT = "user";
1929    private static final String TAG_LAUNCHER = "launcher";
1930
1931    private static final String ATTR_VALUE = "value";
1932
1933    @UserIdInt
1934    final int mUserId;
1935
1936    private final ArrayMap<String, PackageShortcuts> mPackages = new ArrayMap<>();
1937
1938    private final ArrayMap<String, LauncherShortcuts> mLaunchers = new ArrayMap<>();
1939
1940    private ComponentName mLauncherComponent;
1941
1942    public UserShortcuts(int userId) {
1943        mUserId = userId;
1944    }
1945
1946    public ArrayMap<String, PackageShortcuts> getPackages() {
1947        return mPackages;
1948    }
1949
1950    public ArrayMap<String, LauncherShortcuts> getLaunchers() {
1951        return mLaunchers;
1952    }
1953
1954    public PackageShortcuts getPackageShortcuts(@NonNull String packageName) {
1955        PackageShortcuts ret = mPackages.get(packageName);
1956        if (ret == null) {
1957            ret = new PackageShortcuts(mUserId, packageName);
1958            mPackages.put(packageName, ret);
1959        }
1960        return ret;
1961    }
1962
1963    public LauncherShortcuts getLauncherShortcuts(@NonNull String packageName) {
1964        LauncherShortcuts ret = mLaunchers.get(packageName);
1965        if (ret == null) {
1966            ret = new LauncherShortcuts(mUserId, packageName);
1967            mLaunchers.put(packageName, ret);
1968        }
1969        return ret;
1970    }
1971
1972    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
1973        out.startTag(null, TAG_ROOT);
1974
1975        ShortcutService.writeTagValue(out, TAG_LAUNCHER,
1976                mLauncherComponent);
1977
1978        final int lsize = mLaunchers.size();
1979        for (int i = 0; i < lsize; i++) {
1980            mLaunchers.valueAt(i).saveToXml(out);
1981        }
1982
1983        final int psize = mPackages.size();
1984        for (int i = 0; i < psize; i++) {
1985            mPackages.valueAt(i).saveToXml(out);
1986        }
1987
1988        out.endTag(null, TAG_ROOT);
1989    }
1990
1991    public static UserShortcuts loadFromXml(XmlPullParser parser, int userId)
1992            throws IOException, XmlPullParserException {
1993        final UserShortcuts ret = new UserShortcuts(userId);
1994
1995        final int outerDepth = parser.getDepth();
1996        int type;
1997        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
1998                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
1999            if (type != XmlPullParser.START_TAG) {
2000                continue;
2001            }
2002            final int depth = parser.getDepth();
2003            final String tag = parser.getName();
2004            switch (tag) {
2005                case TAG_LAUNCHER: {
2006                    ret.mLauncherComponent = ShortcutService.parseComponentNameAttribute(
2007                            parser, ATTR_VALUE);
2008                    continue;
2009                }
2010                case PackageShortcuts.TAG_ROOT: {
2011                    final PackageShortcuts shortcuts = PackageShortcuts.loadFromXml(parser, userId);
2012
2013                    // Don't use addShortcut(), we don't need to save the icon.
2014                    ret.getPackages().put(shortcuts.mPackageName, shortcuts);
2015                    continue;
2016                }
2017
2018                case LauncherShortcuts.TAG_ROOT: {
2019                    final LauncherShortcuts shortcuts =
2020                            LauncherShortcuts.loadFromXml(parser, userId);
2021
2022                    ret.getLaunchers().put(shortcuts.mPackageName, shortcuts);
2023                    continue;
2024                }
2025            }
2026            throw ShortcutService.throwForInvalidTag(depth, tag);
2027        }
2028        return ret;
2029    }
2030
2031    public ComponentName getLauncherComponent() {
2032        return mLauncherComponent;
2033    }
2034
2035    public void setLauncherComponent(ShortcutService s, ComponentName launcherComponent) {
2036        if (Objects.equal(mLauncherComponent, launcherComponent)) {
2037            return;
2038        }
2039        mLauncherComponent = launcherComponent;
2040        s.scheduleSaveUser(mUserId);
2041    }
2042
2043    public void resetThrottling() {
2044        for (int i = mPackages.size() - 1; i >= 0; i--) {
2045            mPackages.valueAt(i).resetThrottling();
2046        }
2047    }
2048
2049    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
2050        pw.print(prefix);
2051        pw.print("User: ");
2052        pw.print(mUserId);
2053        pw.println();
2054
2055        pw.print(prefix);
2056        pw.print("  ");
2057        pw.print("Default launcher: ");
2058        pw.print(mLauncherComponent);
2059        pw.println();
2060
2061        for (int i = 0; i < mLaunchers.size(); i++) {
2062            mLaunchers.valueAt(i).dump(s, pw, prefix + "  ");
2063        }
2064
2065        for (int i = 0; i < mPackages.size(); i++) {
2066            mPackages.valueAt(i).dump(s, pw, prefix + "  ");
2067        }
2068    }
2069}
2070
2071class LauncherShortcuts {
2072    private static final String TAG = ShortcutService.TAG;
2073
2074    static final String TAG_ROOT = "launcher-pins";
2075
2076    private static final String TAG_PACKAGE = "package";
2077    private static final String TAG_PIN = "pin";
2078
2079    private static final String ATTR_VALUE = "value";
2080    private static final String ATTR_PACKAGE_NAME = "package-name";
2081
2082    @UserIdInt
2083    final int mUserId;
2084
2085    @NonNull
2086    final String mPackageName;
2087
2088    /**
2089     * Package name -> IDs.
2090     */
2091    final private ArrayMap<String, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
2092
2093    LauncherShortcuts(@UserIdInt int userId, @NonNull String packageName) {
2094        mUserId = userId;
2095        mPackageName = packageName;
2096    }
2097
2098    public void pinShortcuts(@NonNull ShortcutService s, @NonNull String packageName,
2099            @NonNull List<String> ids) {
2100        final int idSize = ids.size();
2101        if (idSize == 0) {
2102            mPinnedShortcuts.remove(packageName);
2103        } else {
2104            final ArraySet<String> prevSet = mPinnedShortcuts.get(packageName);
2105
2106            // Pin shortcuts.  Make sure only pin the ones that were visible to the caller.
2107            // i.e. a non-dynamic, pinned shortcut by *other launchers* shouldn't be pinned here.
2108
2109            final PackageShortcuts packageShortcuts =
2110                    s.getPackageShortcutsLocked(packageName, mUserId);
2111            final ArraySet<String> newSet = new ArraySet<>();
2112
2113            for (int i = 0; i < idSize; i++) {
2114                final String id = ids.get(i);
2115                final ShortcutInfo si = packageShortcuts.findShortcutById(id);
2116                if (si == null) {
2117                    continue;
2118                }
2119                if (si.isDynamic() || (prevSet != null && prevSet.contains(id))) {
2120                    newSet.add(id);
2121                }
2122            }
2123            mPinnedShortcuts.put(packageName, newSet);
2124        }
2125        s.getPackageShortcutsLocked(packageName, mUserId).refreshPinnedFlags(s);
2126    }
2127
2128    /**
2129     * Return the pinned shortcut IDs for the publisher package.
2130     */
2131    public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName) {
2132        return mPinnedShortcuts.get(packageName);
2133    }
2134
2135    /**
2136     * Persist.
2137     */
2138    public void saveToXml(XmlSerializer out) throws IOException {
2139        out.startTag(null, TAG_ROOT);
2140        ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME,
2141                mPackageName);
2142
2143        final int size = mPinnedShortcuts.size();
2144        for (int i = 0; i < size; i++) {
2145            out.startTag(null, TAG_PACKAGE);
2146            ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME,
2147                    mPinnedShortcuts.keyAt(i));
2148
2149            final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
2150            final int idSize = ids.size();
2151            for (int j = 0; j < idSize; j++) {
2152                ShortcutService.writeTagValue(out, TAG_PIN, ids.valueAt(j));
2153            }
2154            out.endTag(null, TAG_PACKAGE);
2155        }
2156
2157        out.endTag(null, TAG_ROOT);
2158    }
2159
2160    /**
2161     * Load.
2162     */
2163    public static LauncherShortcuts loadFromXml(XmlPullParser parser, int userId)
2164            throws IOException, XmlPullParserException {
2165        final String launcherPackageName = ShortcutService.parseStringAttribute(parser,
2166                ATTR_PACKAGE_NAME);
2167
2168        final LauncherShortcuts ret = new LauncherShortcuts(userId, launcherPackageName);
2169
2170        ArraySet<String> ids = null;
2171        final int outerDepth = parser.getDepth();
2172        int type;
2173        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2174                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2175            if (type != XmlPullParser.START_TAG) {
2176                continue;
2177            }
2178            final int depth = parser.getDepth();
2179            final String tag = parser.getName();
2180            switch (tag) {
2181                case TAG_PACKAGE: {
2182                    final String packageName = ShortcutService.parseStringAttribute(parser,
2183                            ATTR_PACKAGE_NAME);
2184                    ids = new ArraySet<>();
2185                    ret.mPinnedShortcuts.put(packageName, ids);
2186                    continue;
2187                }
2188                case TAG_PIN: {
2189                    ids.add(ShortcutService.parseStringAttribute(parser,
2190                            ATTR_VALUE));
2191                    continue;
2192                }
2193            }
2194            throw ShortcutService.throwForInvalidTag(depth, tag);
2195        }
2196        return ret;
2197    }
2198
2199    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
2200        pw.println();
2201
2202        pw.print(prefix);
2203        pw.print("Launcher: ");
2204        pw.print(mPackageName);
2205        pw.println();
2206
2207        final int size = mPinnedShortcuts.size();
2208        for (int i = 0; i < size; i++) {
2209            pw.println();
2210
2211            pw.print(prefix);
2212            pw.print("  ");
2213            pw.print("Package: ");
2214            pw.println(mPinnedShortcuts.keyAt(i));
2215
2216            final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
2217            final int idSize = ids.size();
2218
2219            for (int j = 0; j < idSize; j++) {
2220                pw.print(prefix);
2221                pw.print("    ");
2222                pw.print(ids.valueAt(j));
2223                pw.println();
2224            }
2225        }
2226    }
2227}
2228
2229/**
2230 * All the information relevant to shortcuts from a single package (per-user).
2231 */
2232class PackageShortcuts {
2233    private static final String TAG = ShortcutService.TAG;
2234
2235    static final String TAG_ROOT = "package";
2236    private static final String TAG_INTENT_EXTRAS = "intent-extras";
2237    private static final String TAG_EXTRAS = "extras";
2238    private static final String TAG_SHORTCUT = "shortcut";
2239
2240    private static final String ATTR_NAME = "name";
2241    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
2242    private static final String ATTR_CALL_COUNT = "call-count";
2243    private static final String ATTR_LAST_RESET = "last-reset";
2244    private static final String ATTR_ID = "id";
2245    private static final String ATTR_ACTIVITY = "activity";
2246    private static final String ATTR_TITLE = "title";
2247    private static final String ATTR_INTENT = "intent";
2248    private static final String ATTR_WEIGHT = "weight";
2249    private static final String ATTR_TIMESTAMP = "timestamp";
2250    private static final String ATTR_FLAGS = "flags";
2251    private static final String ATTR_ICON_RES = "icon-res";
2252    private static final String ATTR_BITMAP_PATH = "bitmap-path";
2253
2254    @UserIdInt
2255    final int mUserId;
2256
2257    @NonNull
2258    final String mPackageName;
2259
2260    /**
2261     * All the shortcuts from the package, keyed on IDs.
2262     */
2263    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
2264
2265    /**
2266     * # of dynamic shortcuts.
2267     */
2268    private int mDynamicShortcutCount = 0;
2269
2270    /**
2271     * # of times the package has called rate-limited APIs.
2272     */
2273    private int mApiCallCount;
2274
2275    /**
2276     * When {@link #mApiCallCount} was reset last time.
2277     */
2278    private long mLastResetTime;
2279
2280    PackageShortcuts(int userId, String packageName) {
2281        mUserId = userId;
2282        mPackageName = packageName;
2283    }
2284
2285    @Nullable
2286    public ShortcutInfo findShortcutById(String id) {
2287        return mShortcuts.get(id);
2288    }
2289
2290    private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
2291            @NonNull String id) {
2292        final ShortcutInfo shortcut = mShortcuts.remove(id);
2293        if (shortcut != null) {
2294            s.removeIcon(mUserId, shortcut);
2295            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
2296        }
2297        return shortcut;
2298    }
2299
2300    void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
2301        deleteShortcut(s, newShortcut.getId());
2302        s.saveIconAndFixUpShortcut(mUserId, newShortcut);
2303        mShortcuts.put(newShortcut.getId(), newShortcut);
2304    }
2305
2306    /**
2307     * Add a shortcut, or update one with the same ID, with taking over existing flags.
2308     *
2309     * It checks the max number of dynamic shortcuts.
2310     */
2311    public void addDynamicShortcut(@NonNull ShortcutService s,
2312            @NonNull ShortcutInfo newShortcut) {
2313        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
2314
2315        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
2316
2317        final boolean wasPinned;
2318        final int newDynamicCount;
2319
2320        if (oldShortcut == null) {
2321            wasPinned = false;
2322            newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
2323        } else {
2324            wasPinned = oldShortcut.isPinned();
2325            if (oldShortcut.isDynamic()) {
2326                newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
2327            } else {
2328                newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
2329            }
2330        }
2331
2332        // Make sure there's still room.
2333        s.enforceMaxDynamicShortcuts(newDynamicCount);
2334
2335        // Okay, make it dynamic and add.
2336        if (wasPinned) {
2337            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
2338        }
2339
2340        addShortcut(s, newShortcut);
2341        mDynamicShortcutCount = newDynamicCount;
2342    }
2343
2344    /**
2345     * Remove all shortcuts that aren't pinned nor dynamic.
2346     */
2347    private void removeOrphans(@NonNull ShortcutService s) {
2348        ArrayList<String> removeList = null; // Lazily initialize.
2349
2350        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
2351            final ShortcutInfo si = mShortcuts.valueAt(i);
2352
2353            if (si.isPinned() || si.isDynamic()) continue;
2354
2355            if (removeList == null) {
2356                removeList = new ArrayList<>();
2357            }
2358            removeList.add(si.getId());
2359        }
2360        if (removeList != null) {
2361            for (int i = removeList.size() - 1 ; i >= 0; i--) {
2362                deleteShortcut(s, removeList.get(i));
2363            }
2364        }
2365    }
2366
2367    /**
2368     * Remove all dynamic shortcuts.
2369     */
2370    public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
2371        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
2372            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
2373        }
2374        removeOrphans(s);
2375        mDynamicShortcutCount = 0;
2376    }
2377
2378    /**
2379     * Remove a dynamic shortcut by ID.
2380     */
2381    public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
2382        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
2383
2384        if (oldShortcut == null) {
2385            return;
2386        }
2387        if (oldShortcut.isDynamic()) {
2388            mDynamicShortcutCount--;
2389        }
2390        if (oldShortcut.isPinned()) {
2391            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
2392        } else {
2393            deleteShortcut(s, shortcutId);
2394        }
2395    }
2396
2397    /**
2398     * Called after a launcher updates the pinned set.  For each shortcut in this package,
2399     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
2400     *
2401     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
2402     */
2403    public void refreshPinnedFlags(@NonNull ShortcutService s) {
2404        // First, un-pin all shortcuts
2405        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
2406            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
2407        }
2408
2409        // Then, for the pinned set for each launcher, set the pin flag one by one.
2410        final ArrayMap<String, LauncherShortcuts> launchers =
2411                s.getUserShortcutsLocked(mUserId).getLaunchers();
2412
2413        for (int l = launchers.size() - 1; l >= 0; l--) {
2414            final LauncherShortcuts launcherShortcuts = launchers.valueAt(l);
2415            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(mPackageName);
2416
2417            if (pinned == null || pinned.size() == 0) {
2418                continue;
2419            }
2420            for (int i = pinned.size() - 1; i >= 0; i--) {
2421                final ShortcutInfo si = mShortcuts.get(pinned.valueAt(i));
2422                if (si == null) {
2423                    s.wtf("Shortcut not found");
2424                } else {
2425                    si.addFlags(ShortcutInfo.FLAG_PINNED);
2426                }
2427            }
2428        }
2429
2430        // Lastly, remove the ones that are no longer pinned nor dynamic.
2431        removeOrphans(s);
2432    }
2433
2434    /**
2435     * Number of calls that the caller has made, since the last reset.
2436     */
2437    public int getApiCallCount(@NonNull ShortcutService s) {
2438        final long last = s.getLastResetTimeLocked();
2439
2440        final long now = s.injectCurrentTimeMillis();
2441        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
2442            Slog.w(TAG, "Clock rewound");
2443            // Clock rewound.
2444            mLastResetTime = now;
2445            mApiCallCount = 0;
2446            return mApiCallCount;
2447        }
2448
2449        // If not reset yet, then reset.
2450        if (mLastResetTime < last) {
2451            if (ShortcutService.DEBUG) {
2452                Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting",
2453                        mLastResetTime, now, last));
2454            }
2455            mApiCallCount = 0;
2456            mLastResetTime = last;
2457        }
2458        return mApiCallCount;
2459    }
2460
2461    /**
2462     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
2463     * and return true.  Otherwise just return false.
2464     */
2465    public boolean tryApiCall(@NonNull ShortcutService s) {
2466        if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
2467            return false;
2468        }
2469        mApiCallCount++;
2470        return true;
2471    }
2472
2473    public void resetRateLimitingForCommandLine() {
2474        mApiCallCount = 0;
2475        mLastResetTime = 0;
2476    }
2477
2478    /**
2479     * Find all shortcuts that match {@code query}.
2480     */
2481    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
2482            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
2483            @Nullable String callingLauncher) {
2484
2485        // Set of pinned shortcuts by the calling launcher.
2486        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
2487                : s.getLauncherShortcuts(callingLauncher, mUserId)
2488                    .getPinnedShortcutIds(mPackageName);
2489
2490        for (int i = 0; i < mShortcuts.size(); i++) {
2491            final ShortcutInfo si = mShortcuts.valueAt(i);
2492
2493            // If it's called by non-launcher (i.e. publisher, always include -> true.
2494            // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
2495            // it.
2496            final boolean isPinnedByCaller = (callingLauncher == null)
2497                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
2498            if (!si.isDynamic()) {
2499                if (!si.isPinned()) {
2500                    s.wtf("Shortcut not pinned here");
2501                    continue;
2502                }
2503                if (!isPinnedByCaller) {
2504                    continue;
2505                }
2506            }
2507            final ShortcutInfo clone = si.clone(cloneFlag);
2508            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
2509            // since it may check isPinned.
2510            if (!isPinnedByCaller) {
2511                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
2512            }
2513            if (query == null || query.test(clone)) {
2514                result.add(clone);
2515            }
2516        }
2517    }
2518
2519    public void resetThrottling() {
2520        mApiCallCount = 0;
2521    }
2522
2523    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
2524        pw.println();
2525
2526        pw.print(prefix);
2527        pw.print("Package: ");
2528        pw.print(mPackageName);
2529        pw.println();
2530
2531        pw.print(prefix);
2532        pw.print("  ");
2533        pw.print("Calls: ");
2534        pw.print(getApiCallCount(s));
2535        pw.println();
2536
2537        // This should be after getApiCallCount(), which may update it.
2538        pw.print(prefix);
2539        pw.print("  ");
2540        pw.print("Last reset: [");
2541        pw.print(mLastResetTime);
2542        pw.print("] ");
2543        pw.print(s.formatTime(mLastResetTime));
2544        pw.println();
2545
2546        pw.println("      Shortcuts:");
2547        long totalBitmapSize = 0;
2548        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
2549        final int size = shortcuts.size();
2550        for (int i = 0; i < size; i++) {
2551            final ShortcutInfo si = shortcuts.valueAt(i);
2552            pw.print("        ");
2553            pw.println(si.toInsecureString());
2554            if (si.getBitmapPath() != null) {
2555                final long len = new File(si.getBitmapPath()).length();
2556                pw.print("          ");
2557                pw.print("bitmap size=");
2558                pw.println(len);
2559
2560                totalBitmapSize += len;
2561            }
2562        }
2563        pw.print(prefix);
2564        pw.print("  ");
2565        pw.print("Total bitmap size: ");
2566        pw.print(totalBitmapSize);
2567        pw.print(" (");
2568        pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
2569        pw.println(")");
2570    }
2571
2572    public void saveToXml(@NonNull XmlSerializer out) throws IOException, XmlPullParserException {
2573        out.startTag(null, TAG_ROOT);
2574
2575        ShortcutService.writeAttr(out, ATTR_NAME, mPackageName);
2576        ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
2577        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
2578        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
2579
2580        final int size = mShortcuts.size();
2581        for (int j = 0; j < size; j++) {
2582            saveShortcut(out, mShortcuts.valueAt(j));
2583        }
2584
2585        out.endTag(null, TAG_ROOT);
2586    }
2587
2588    private static void saveShortcut(XmlSerializer out, ShortcutInfo si)
2589            throws IOException, XmlPullParserException {
2590        out.startTag(null, TAG_SHORTCUT);
2591        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
2592        // writeAttr(out, "package", si.getPackageName()); // not needed
2593        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
2594        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
2595        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
2596        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
2597        ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight());
2598        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
2599                si.getLastChangedTimestamp());
2600        ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
2601        ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
2602        ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
2603
2604        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
2605                si.getIntentPersistableExtras());
2606        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
2607
2608        out.endTag(null, TAG_SHORTCUT);
2609    }
2610
2611    public static PackageShortcuts loadFromXml(XmlPullParser parser, int userId)
2612            throws IOException, XmlPullParserException {
2613
2614        final String packageName = ShortcutService.parseStringAttribute(parser,
2615                ATTR_NAME);
2616
2617        final PackageShortcuts ret = new PackageShortcuts(userId, packageName);
2618
2619        ret.mDynamicShortcutCount =
2620                ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
2621        ret.mApiCallCount =
2622                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
2623        ret.mLastResetTime =
2624                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
2625
2626        final int outerDepth = parser.getDepth();
2627        int type;
2628        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2629                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2630            if (type != XmlPullParser.START_TAG) {
2631                continue;
2632            }
2633            final int depth = parser.getDepth();
2634            final String tag = parser.getName();
2635            switch (tag) {
2636                case TAG_SHORTCUT:
2637                    final ShortcutInfo si = parseShortcut(parser, packageName);
2638
2639                    // Don't use addShortcut(), we don't need to save the icon.
2640                    ret.mShortcuts.put(si.getId(), si);
2641                    continue;
2642            }
2643            throw ShortcutService.throwForInvalidTag(depth, tag);
2644        }
2645        return ret;
2646    }
2647
2648    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName)
2649            throws IOException, XmlPullParserException {
2650        String id;
2651        ComponentName activityComponent;
2652        // Icon icon;
2653        String title;
2654        Intent intent;
2655        PersistableBundle intentPersistableExtras = null;
2656        int weight;
2657        PersistableBundle extras = null;
2658        long lastChangedTimestamp;
2659        int flags;
2660        int iconRes;
2661        String bitmapPath;
2662
2663        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
2664        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
2665                ATTR_ACTIVITY);
2666        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
2667        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
2668        weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT);
2669        lastChangedTimestamp = (int) ShortcutService.parseLongAttribute(parser,
2670                ATTR_TIMESTAMP);
2671        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
2672        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
2673        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
2674
2675        final int outerDepth = parser.getDepth();
2676        int type;
2677        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2678                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2679            if (type != XmlPullParser.START_TAG) {
2680                continue;
2681            }
2682            final int depth = parser.getDepth();
2683            final String tag = parser.getName();
2684            if (ShortcutService.DEBUG_LOAD) {
2685                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
2686                        depth, type, tag));
2687            }
2688            switch (tag) {
2689                case TAG_INTENT_EXTRAS:
2690                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
2691                    continue;
2692                case TAG_EXTRAS:
2693                    extras = PersistableBundle.restoreFromXml(parser);
2694                    continue;
2695            }
2696            throw ShortcutService.throwForInvalidTag(depth, tag);
2697        }
2698        return new ShortcutInfo(
2699                id, packageName, activityComponent, /* icon =*/ null, title, intent,
2700                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
2701                iconRes, bitmapPath);
2702    }
2703}
2704