ShortcutService.java revision 5504622fb01ab9774b5e73d05f86ee03a8b68ab7
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.ParceledListSlice;
32import android.content.pm.ShortcutInfo;
33import android.content.pm.ShortcutServiceInternal;
34import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
35import android.graphics.Bitmap;
36import android.graphics.Bitmap.CompressFormat;
37import android.graphics.BitmapFactory;
38import android.graphics.Canvas;
39import android.graphics.RectF;
40import android.graphics.drawable.Icon;
41import android.net.Uri;
42import android.os.Binder;
43import android.os.Environment;
44import android.os.Handler;
45import android.os.ParcelFileDescriptor;
46import android.os.PersistableBundle;
47import android.os.Process;
48import android.os.RemoteException;
49import android.os.ResultReceiver;
50import android.os.SELinux;
51import android.os.ShellCommand;
52import android.os.UserHandle;
53import android.text.TextUtils;
54import android.text.format.Formatter;
55import android.text.format.Time;
56import android.util.ArrayMap;
57import android.util.ArraySet;
58import android.util.AtomicFile;
59import android.util.Slog;
60import android.util.SparseArray;
61import android.util.TypedValue;
62import android.util.Xml;
63
64import com.android.internal.annotations.GuardedBy;
65import com.android.internal.annotations.VisibleForTesting;
66import com.android.internal.os.BackgroundThread;
67import com.android.internal.util.FastXmlSerializer;
68import com.android.internal.util.Preconditions;
69import com.android.server.LocalServices;
70import com.android.server.SystemService;
71
72import libcore.io.IoUtils;
73
74import org.xmlpull.v1.XmlPullParser;
75import org.xmlpull.v1.XmlPullParserException;
76import org.xmlpull.v1.XmlSerializer;
77
78import java.io.File;
79import java.io.FileDescriptor;
80import java.io.FileInputStream;
81import java.io.FileNotFoundException;
82import java.io.FileOutputStream;
83import java.io.IOException;
84import java.io.InputStream;
85import java.io.PrintWriter;
86import java.net.URISyntaxException;
87import java.nio.charset.StandardCharsets;
88import java.util.ArrayList;
89import java.util.List;
90import java.util.function.Predicate;
91
92/**
93 * TODO:
94 *
95 * - Implement launchShortcut
96 *
97 * - Detect when already registered instances are passed to APIs again, which might break
98 *   internal bitmap handling.
99 *
100 * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res
101 *   -> Need to scan all packages when a user starts too.
102 *   -> Clear data -> remove all dynamic?  but not the pinned?
103 *
104 * - Pinned per each launcher package (multiple launchers)
105 *
106 * - Load config from settings
107 *
108 * - Make save async (should we?)
109 *
110 * - Scan and remove orphan bitmaps (just in case).
111 *
112 * - Backup & restore
113 */
114public class ShortcutService extends IShortcutService.Stub {
115    static final String TAG = "ShortcutService";
116
117    private static final boolean DEBUG = true; // STOPSHIP if true
118    private static final boolean DEBUG_LOAD = true; // STOPSHIP if true
119
120    private static final int DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day
121    private static final int DEFAULT_MAX_DAILY_UPDATES = 10;
122    private static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5;
123    private static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
124    private static final int DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP = 48;
125
126    private static final int SAVE_DELAY_MS = 5000; // in milliseconds.
127
128    @VisibleForTesting
129    static final String FILENAME_BASE_STATE = "shortcut_service.xml";
130
131    @VisibleForTesting
132    static final String DIRECTORY_PER_USER = "shortcut_service";
133
134    @VisibleForTesting
135    static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
136
137    static final String DIRECTORY_BITMAPS = "bitmaps";
138
139    private static final String TAG_ROOT = "root";
140    private static final String TAG_PACKAGE = "package";
141    private static final String TAG_LAST_RESET_TIME = "last_reset_time";
142    private static final String TAG_INTENT_EXTRAS = "intent-extras";
143    private static final String TAG_EXTRAS = "extras";
144    private static final String TAG_SHORTCUT = "shortcut";
145
146    private static final String ATTR_VALUE = "value";
147    private static final String ATTR_NAME = "name";
148    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
149    private static final String ATTR_CALL_COUNT = "call-count";
150    private static final String ATTR_LAST_RESET = "last-reset";
151    private static final String ATTR_ID = "id";
152    private static final String ATTR_ACTIVITY = "activity";
153    private static final String ATTR_TITLE = "title";
154    private static final String ATTR_INTENT = "intent";
155    private static final String ATTR_WEIGHT = "weight";
156    private static final String ATTR_TIMESTAMP = "timestamp";
157    private static final String ATTR_FLAGS = "flags";
158    private static final String ATTR_ICON_RES = "icon-res";
159    private static final String ATTR_BITMAP_PATH = "bitmap-path";
160
161    private final Context mContext;
162
163    private final Object mLock = new Object();
164
165    private final Handler mHandler;
166
167    @GuardedBy("mLock")
168    private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1);
169
170    @GuardedBy("mLock")
171    private long mRawLastResetTime;
172
173    /**
174     * All the information relevant to shortcuts from a single package (per-user).
175     *
176     * TODO Move the persisting code to this class.
177     *
178     * Only save/load/dump should look/touch inside this class.
179     */
180    private static class PackageShortcuts {
181        @UserIdInt
182        private final int mUserId;
183
184        @NonNull
185        private final String mPackageName;
186
187        /**
188         * All the shortcuts from the package, keyed on IDs.
189         */
190        final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
191
192        /**
193         * # of dynamic shortcuts.
194         */
195        private int mDynamicShortcutCount = 0;
196
197        /**
198         * # of times the package has called rate-limited APIs.
199         */
200        private int mApiCallCount;
201
202        /**
203         * When {@link #mApiCallCount} was reset last time.
204         */
205        private long mLastResetTime;
206
207        private PackageShortcuts(int userId, String packageName) {
208            mUserId = userId;
209            mPackageName = packageName;
210        }
211
212        @GuardedBy("mLock")
213        @Nullable
214        public ShortcutInfo findShortcutById(String id) {
215            return mShortcuts.get(id);
216        }
217
218        private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
219                @NonNull String id) {
220            final ShortcutInfo shortcut = mShortcuts.remove(id);
221            if (shortcut != null) {
222                s.removeIcon(mUserId, shortcut);
223                shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
224            }
225            return shortcut;
226        }
227
228        void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
229            deleteShortcut(s, newShortcut.getId());
230            s.saveIconAndFixUpShortcut(mUserId, newShortcut);
231            mShortcuts.put(newShortcut.getId(), newShortcut);
232        }
233
234        /**
235         * Add a shortcut, or update one with the same ID, with taking over existing flags.
236         *
237         * It checks the max number of dynamic shortcuts.
238         */
239        @GuardedBy("mLock")
240        public void updateShortcutWithCapping(@NonNull ShortcutService s,
241                @NonNull ShortcutInfo newShortcut) {
242            final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
243
244            int oldFlags = 0;
245            int newDynamicCount = mDynamicShortcutCount;
246
247            if (oldShortcut != null) {
248                oldFlags = oldShortcut.getFlags();
249                if (oldShortcut.isDynamic()) {
250                    newDynamicCount--;
251                }
252            }
253            if (newShortcut.isDynamic()) {
254                newDynamicCount++;
255            }
256            // Make sure there's still room.
257            s.enforceMaxDynamicShortcuts(newDynamicCount);
258
259            // Okay, make it dynamic and add.
260            newShortcut.addFlags(oldFlags);
261
262            addShortcut(s, newShortcut);
263            mDynamicShortcutCount = newDynamicCount;
264        }
265
266        /**
267         * Remove all shortcuts that aren't pinned nor dynamic.
268         */
269        private void removeOrphans(@NonNull ShortcutService s) {
270            ArrayList<String> removeList = null; // Lazily initialize.
271
272            for (int i = mShortcuts.size() - 1; i >= 0; i--) {
273                final ShortcutInfo si = mShortcuts.valueAt(i);
274
275                if (si.isPinned() || si.isDynamic()) continue;
276
277                if (removeList == null) {
278                    removeList = new ArrayList<>();
279                }
280                removeList.add(si.getId());
281            }
282            if (removeList != null) {
283                for (int i = removeList.size() - 1 ; i >= 0; i--) {
284                    deleteShortcut(s, removeList.get(i));
285                }
286            }
287        }
288
289        @GuardedBy("mLock")
290        public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
291            for (int i = mShortcuts.size() - 1; i >= 0; i--) {
292                mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
293            }
294            removeOrphans(s);
295            mDynamicShortcutCount = 0;
296        }
297
298        @GuardedBy("mLock")
299        public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
300            final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
301
302            if (oldShortcut == null) {
303                return;
304            }
305            if (oldShortcut.isDynamic()) {
306                mDynamicShortcutCount--;
307            }
308            if (oldShortcut.isPinned()) {
309                oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
310            } else {
311                deleteShortcut(s, shortcutId);
312            }
313        }
314
315        @GuardedBy("mLock")
316        public void replacePinned(@NonNull ShortcutService s, String launcherPackage,
317                List<String> shortcutIds) {
318
319            // TODO Should be per launcherPackage.
320
321            // First, un-pin all shortcuts
322            for (int i = mShortcuts.size() - 1; i >= 0; i--) {
323                mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
324            }
325
326            // Then pin ALL
327            for (int i = shortcutIds.size() - 1; i >= 0; i--) {
328                final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i));
329                if (shortcut != null) {
330                    shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
331                }
332            }
333
334            removeOrphans(s);
335        }
336
337        /**
338         * Number of calls that the caller has made, since the last reset.
339         */
340        @GuardedBy("mLock")
341        public int getApiCallCount(@NonNull ShortcutService s) {
342            final long last = s.getLastResetTimeLocked();
343
344            final long now = s.injectCurrentTimeMillis();
345            if (mLastResetTime > now) {
346                // Clock rewound. // TODO Test it
347                mLastResetTime = now;
348            }
349
350            // If not reset yet, then reset.
351            if (mLastResetTime < last) {
352                mApiCallCount = 0;
353                mLastResetTime = last;
354            }
355            return mApiCallCount;
356        }
357
358        /**
359         * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
360         * and return true.  Otherwise just return false.
361         */
362        @GuardedBy("mLock")
363        public boolean tryApiCall(@NonNull ShortcutService s) {
364            if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
365                return false;
366            }
367            mApiCallCount++;
368            return true;
369        }
370
371        @GuardedBy("mLock")
372        public void resetRateLimitingForCommandLine() {
373            mApiCallCount = 0;
374            mLastResetTime = 0;
375        }
376
377        /**
378         * Find all shortcuts that match {@code query}.
379         */
380        @GuardedBy("mLock")
381        public void findAll(@NonNull List<ShortcutInfo> result,
382                @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
383            for (int i = 0; i < mShortcuts.size(); i++) {
384                final ShortcutInfo si = mShortcuts.valueAt(i);
385                if (query == null || query.test(si)) {
386                    result.add(si.clone(cloneFlag));
387                }
388            }
389        }
390    }
391
392    /**
393     * User ID -> package name -> list of ShortcutInfos.
394     */
395    @GuardedBy("mLock")
396    private final SparseArray<ArrayMap<String, PackageShortcuts>> mShortcuts =
397            new SparseArray<>();
398
399    /**
400     * Max number of dynamic shortcuts that each application can have at a time.
401     */
402    private int mMaxDynamicShortcuts;
403
404    /**
405     * Max number of updating API calls that each application can make a day.
406     */
407    private int mMaxDailyUpdates;
408
409    /**
410     * Actual throttling-reset interval.  By default it's a day.
411     */
412    private long mResetInterval;
413
414    /**
415     * Icon max width/height in pixels.
416     */
417    private int mMaxIconDimension;
418
419    private CompressFormat mIconPersistFormat = CompressFormat.PNG;
420
421    private int mIconPersistQuality = 100;
422
423    public ShortcutService(Context context) {
424        mContext = Preconditions.checkNotNull(context);
425        LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
426        mHandler = new Handler(BackgroundThread.get().getLooper());
427    }
428
429    /**
430     * System service lifecycle.
431     */
432    public static final class Lifecycle extends SystemService {
433        final ShortcutService mService;
434
435        public Lifecycle(Context context) {
436            super(context);
437            mService = new ShortcutService(context);
438        }
439
440        @Override
441        public void onStart() {
442            publishBinderService(Context.SHORTCUT_SERVICE, mService);
443        }
444
445        @Override
446        public void onBootPhase(int phase) {
447            mService.onBootPhase(phase);
448        }
449
450        @Override
451        public void onCleanupUser(int userHandle) {
452            synchronized (mService.mLock) {
453                mService.onCleanupUserInner(userHandle);
454            }
455        }
456
457        @Override
458        public void onStartUser(int userId) {
459            synchronized (mService.mLock) {
460                mService.onStartUserLocked(userId);
461            }
462        }
463    }
464
465    /** lifecycle event */
466    void onBootPhase(int phase) {
467        if (DEBUG) {
468            Slog.d(TAG, "onBootPhase: " + phase);
469        }
470        switch (phase) {
471            case SystemService.PHASE_LOCK_SETTINGS_READY:
472                initialize();
473                break;
474        }
475    }
476
477    /** lifecycle event */
478    void onStartUserLocked(int userId) {
479        // Preload
480        getUserShortcutsLocked(userId);
481    }
482
483    /** lifecycle event */
484    void onCleanupUserInner(int userId) {
485        // Unload
486        mShortcuts.delete(userId);
487    }
488
489    /** Return the base state file name */
490    private AtomicFile getBaseStateFile() {
491        final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE);
492        path.mkdirs();
493        return new AtomicFile(path);
494    }
495
496    /**
497     * Init the instance. (load the state file, etc)
498     */
499    private void initialize() {
500        synchronized (mLock) {
501            injectLoadConfigurationLocked();
502            loadBaseStateLocked();
503        }
504    }
505
506    // Test overrides it to inject different values.
507    @VisibleForTesting
508    void injectLoadConfigurationLocked() {
509        mResetInterval = DEFAULT_RESET_INTERVAL_SEC * 1000L;
510        mMaxDailyUpdates = DEFAULT_MAX_DAILY_UPDATES;
511        mMaxDynamicShortcuts = DEFAULT_MAX_SHORTCUTS_PER_APP;
512
513        final int iconDimensionDp = (injectIsLowRamDevice()
514                ? DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP : DEFAULT_MAX_ICON_DIMENSION_DP);
515        mMaxIconDimension =
516                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, iconDimensionDp,
517                        mContext.getResources().getDisplayMetrics());
518    }
519
520    // === Persisting ===
521
522    @Nullable
523    private String parseStringAttribute(XmlPullParser parser, String attribute) {
524        return parser.getAttributeValue(null, attribute);
525    }
526
527    private long parseLongAttribute(XmlPullParser parser, String attribute) {
528        final String value = parseStringAttribute(parser, attribute);
529        if (TextUtils.isEmpty(value)) {
530            return 0;
531        }
532        try {
533            return Long.parseLong(value);
534        } catch (NumberFormatException e) {
535            Slog.e(TAG, "Error parsing long " + value);
536            return 0;
537        }
538    }
539
540    @Nullable
541    private ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) {
542        final String value = parseStringAttribute(parser, attribute);
543        if (TextUtils.isEmpty(value)) {
544            return null;
545        }
546        return ComponentName.unflattenFromString(value);
547    }
548
549    @Nullable
550    private Intent parseIntentAttribute(XmlPullParser parser, String attribute) {
551        final String value = parseStringAttribute(parser, attribute);
552        if (TextUtils.isEmpty(value)) {
553            return null;
554        }
555        try {
556            return Intent.parseUri(value, /* flags =*/ 0);
557        } catch (URISyntaxException e) {
558            Slog.e(TAG, "Error parsing intent", e);
559            return null;
560        }
561    }
562
563    private void writeTagValue(XmlSerializer out, String tag, String value) throws IOException {
564        if (TextUtils.isEmpty(value)) return;
565
566        out.startTag(null, tag);
567        out.attribute(null, ATTR_VALUE, value);
568        out.endTag(null, tag);
569    }
570
571    private void writeTagValue(XmlSerializer out, String tag, long value) throws IOException {
572        writeTagValue(out, tag, Long.toString(value));
573    }
574
575    private void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle)
576            throws IOException, XmlPullParserException {
577        if (bundle == null) return;
578
579        out.startTag(null, tag);
580        bundle.saveToXml(out);
581        out.endTag(null, tag);
582    }
583
584    private void writeAttr(XmlSerializer out, String name, String value) throws IOException {
585        if (TextUtils.isEmpty(value)) return;
586
587        out.attribute(null, name, value);
588    }
589
590    private void writeAttr(XmlSerializer out, String name, long value) throws IOException {
591        writeAttr(out, name, String.valueOf(value));
592    }
593
594    private void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException {
595        if (comp == null) return;
596        writeAttr(out, name, comp.flattenToString());
597    }
598
599    private void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException {
600        if (intent == null) return;
601
602        writeAttr(out, name, intent.toUri(/* flags =*/ 0));
603    }
604
605    @VisibleForTesting
606    void saveBaseStateLocked() {
607        final AtomicFile file = getBaseStateFile();
608        if (DEBUG) {
609            Slog.i(TAG, "Saving to " + file.getBaseFile());
610        }
611
612        FileOutputStream outs = null;
613        try {
614            outs = file.startWrite();
615
616            // Write to XML
617            XmlSerializer out = new FastXmlSerializer();
618            out.setOutput(outs, StandardCharsets.UTF_8.name());
619            out.startDocument(null, true);
620            out.startTag(null, TAG_ROOT);
621
622            // Body.
623            writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime);
624
625            // Epilogue.
626            out.endTag(null, TAG_ROOT);
627            out.endDocument();
628
629            // Close.
630            file.finishWrite(outs);
631        } catch (IOException e) {
632            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
633            file.failWrite(outs);
634        }
635    }
636
637    private void loadBaseStateLocked() {
638        mRawLastResetTime = 0;
639
640        final AtomicFile file = getBaseStateFile();
641        if (DEBUG) {
642            Slog.i(TAG, "Loading from " + file.getBaseFile());
643        }
644        try (FileInputStream in = file.openRead()) {
645            XmlPullParser parser = Xml.newPullParser();
646            parser.setInput(in, StandardCharsets.UTF_8.name());
647
648            int type;
649            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
650                if (type != XmlPullParser.START_TAG) {
651                    continue;
652                }
653                final int depth = parser.getDepth();
654                // Check the root tag
655                final String tag = parser.getName();
656                if (depth == 1) {
657                    if (!TAG_ROOT.equals(tag)) {
658                        Slog.e(TAG, "Invalid root tag: " + tag);
659                        return;
660                    }
661                    continue;
662                }
663                // Assume depth == 2
664                switch (tag) {
665                    case TAG_LAST_RESET_TIME:
666                        mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE);
667                        break;
668                    default:
669                        Slog.e(TAG, "Invalid tag: " + tag);
670                        break;
671                }
672            }
673        } catch (FileNotFoundException e) {
674            // Use the default
675        } catch (IOException|XmlPullParserException e) {
676            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
677
678            mRawLastResetTime = 0;
679        }
680        // Adjust the last reset time.
681        getLastResetTimeLocked();
682    }
683
684    private void saveUserLocked(@UserIdInt int userId) {
685        final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
686        if (DEBUG) {
687            Slog.i(TAG, "Saving to " + path);
688        }
689        path.mkdirs();
690        final AtomicFile file = new AtomicFile(path);
691        FileOutputStream outs = null;
692        try {
693            outs = file.startWrite();
694
695            // Write to XML
696            XmlSerializer out = new FastXmlSerializer();
697            out.setOutput(outs, StandardCharsets.UTF_8.name());
698            out.startDocument(null, true);
699            out.startTag(null, TAG_ROOT);
700
701            final ArrayMap<String, PackageShortcuts> packages = getUserShortcutsLocked(userId);
702
703            // Body.
704            for (int i = 0; i < packages.size(); i++) {
705                final String packageName = packages.keyAt(i);
706                final PackageShortcuts packageShortcuts = packages.valueAt(i);
707
708                // TODO Move this to PackageShortcuts.
709
710                out.startTag(null, TAG_PACKAGE);
711
712                writeAttr(out, ATTR_NAME, packageName);
713                writeAttr(out, ATTR_DYNAMIC_COUNT, packageShortcuts.mDynamicShortcutCount);
714                writeAttr(out, ATTR_CALL_COUNT, packageShortcuts.mApiCallCount);
715                writeAttr(out, ATTR_LAST_RESET, packageShortcuts.mLastResetTime);
716
717                final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
718                final int size = shortcuts.size();
719                for (int j = 0; j < size; j++) {
720                    saveShortcut(out, shortcuts.valueAt(j));
721                }
722
723                out.endTag(null, TAG_PACKAGE);
724            }
725
726            // Epilogue.
727            out.endTag(null, TAG_ROOT);
728            out.endDocument();
729
730            // Close.
731            file.finishWrite(outs);
732        } catch (IOException|XmlPullParserException e) {
733            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
734            file.failWrite(outs);
735        }
736    }
737
738    private void saveShortcut(XmlSerializer out, ShortcutInfo si)
739            throws IOException, XmlPullParserException {
740        out.startTag(null, TAG_SHORTCUT);
741        writeAttr(out, ATTR_ID, si.getId());
742        // writeAttr(out, "package", si.getPackageName()); // not needed
743        writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
744        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
745        writeAttr(out, ATTR_TITLE, si.getTitle());
746        writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
747        writeAttr(out, ATTR_WEIGHT, si.getWeight());
748        writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp());
749        writeAttr(out, ATTR_FLAGS, si.getFlags());
750        writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
751        writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
752
753        writeTagExtra(out, TAG_INTENT_EXTRAS, si.getIntentPersistableExtras());
754        writeTagExtra(out, TAG_EXTRAS, si.getExtras());
755
756        out.endTag(null, TAG_SHORTCUT);
757    }
758
759    private static IOException throwForInvalidTag(int depth, String tag) throws IOException {
760        throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth));
761    }
762
763    @Nullable
764    private ArrayMap<String, PackageShortcuts> loadUserLocked(@UserIdInt int userId) {
765        final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
766        if (DEBUG) {
767            Slog.i(TAG, "Loading from " + path);
768        }
769        path.mkdirs();
770        final AtomicFile file = new AtomicFile(path);
771
772        final FileInputStream in;
773        try {
774            in = file.openRead();
775        } catch (FileNotFoundException e) {
776            if (DEBUG) {
777                Slog.i(TAG, "Not found " + path);
778            }
779            return null;
780        }
781        final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>();
782        try {
783            XmlPullParser parser = Xml.newPullParser();
784            parser.setInput(in, StandardCharsets.UTF_8.name());
785
786            String packageName = null;
787            PackageShortcuts shortcuts = null;
788
789            int type;
790            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
791                if (type != XmlPullParser.START_TAG) {
792                    continue;
793                }
794                final int depth = parser.getDepth();
795
796                // TODO Move some of this to PackageShortcuts.
797
798                final String tag = parser.getName();
799                if (DEBUG_LOAD) {
800                    Slog.d(TAG, String.format("depth=%d type=%d name=%s",
801                            depth, type, tag));
802                }
803                switch (depth) {
804                    case 1: {
805                        if (TAG_ROOT.equals(tag)) {
806                            continue;
807                        }
808                        break;
809                    }
810                    case 2: {
811                        switch (tag) {
812                            case TAG_PACKAGE:
813                                packageName = parseStringAttribute(parser, ATTR_NAME);
814                                shortcuts = new PackageShortcuts(userId, packageName);
815                                ret.put(packageName, shortcuts);
816
817                                shortcuts.mDynamicShortcutCount =
818                                        (int) parseLongAttribute(parser, ATTR_DYNAMIC_COUNT);
819                                shortcuts.mApiCallCount =
820                                        (int) parseLongAttribute(parser, ATTR_CALL_COUNT);
821                                shortcuts.mLastResetTime = parseLongAttribute(parser,
822                                        ATTR_LAST_RESET);
823                                continue;
824                        }
825                        break;
826                    }
827                    case 3: {
828                        switch (tag) {
829                            case TAG_SHORTCUT:
830                                final ShortcutInfo si = parseShortcut(parser, packageName);
831
832                                // Don't use addShortcut(), we don't need to save the icon.
833                                shortcuts.mShortcuts.put(si.getId(), si);
834                                continue;
835                        }
836                        break;
837                    }
838                }
839                throwForInvalidTag(depth, tag);
840            }
841            return ret;
842        } catch (IOException|XmlPullParserException e) {
843            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
844            return null;
845        } finally {
846            IoUtils.closeQuietly(in);
847        }
848    }
849
850    private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName)
851            throws IOException, XmlPullParserException {
852        String id;
853        ComponentName activityComponent;
854        Icon icon;
855        String title;
856        Intent intent;
857        PersistableBundle intentPersistableExtras = null;
858        int weight;
859        PersistableBundle extras = null;
860        long lastChangedTimestamp;
861        int flags;
862        int iconRes;
863        String bitmapPath;
864
865        id = parseStringAttribute(parser, ATTR_ID);
866        activityComponent = parseComponentNameAttribute(parser, ATTR_ACTIVITY);
867        title = parseStringAttribute(parser, ATTR_TITLE);
868        intent = parseIntentAttribute(parser, ATTR_INTENT);
869        weight = (int) parseLongAttribute(parser, ATTR_WEIGHT);
870        lastChangedTimestamp = (int) parseLongAttribute(parser, ATTR_TIMESTAMP);
871        flags = (int) parseLongAttribute(parser, ATTR_FLAGS);
872        iconRes = (int) parseLongAttribute(parser, ATTR_ICON_RES);
873        bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH);
874
875        final int outerDepth = parser.getDepth();
876        int type;
877        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
878                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
879            if (type != XmlPullParser.START_TAG) {
880                continue;
881            }
882            final int depth = parser.getDepth();
883            final String tag = parser.getName();
884            if (DEBUG_LOAD) {
885                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
886                        depth, type, tag));
887            }
888            switch (tag) {
889                case TAG_INTENT_EXTRAS:
890                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
891                    continue;
892                case TAG_EXTRAS:
893                    extras = PersistableBundle.restoreFromXml(parser);
894                    continue;
895            }
896            throw throwForInvalidTag(depth, tag);
897        }
898        return new ShortcutInfo(
899                id, packgeName, activityComponent, /* icon =*/ null, title, intent,
900                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
901                iconRes, bitmapPath);
902    }
903
904    // TODO Actually make it async.
905    private void scheduleSaveBaseState() {
906        synchronized (mLock) {
907            saveBaseStateLocked();
908        }
909    }
910
911    // TODO Actually make it async.
912    private void scheduleSaveUser(@UserIdInt int userId) {
913        synchronized (mLock) {
914            saveUserLocked(userId);
915        }
916    }
917
918    /** Return the last reset time. */
919    long getLastResetTimeLocked() {
920        updateTimes();
921        return mRawLastResetTime;
922    }
923
924    /** Return the next reset time. */
925    long getNextResetTimeLocked() {
926        updateTimes();
927        return mRawLastResetTime + mResetInterval;
928    }
929
930    /**
931     * Update the last reset time.
932     */
933    private void updateTimes() {
934
935        final long now = injectCurrentTimeMillis();
936
937        final long prevLastResetTime = mRawLastResetTime;
938
939        if (mRawLastResetTime == 0) { // first launch.
940            // TODO Randomize??
941            mRawLastResetTime = now;
942        } else if (now < mRawLastResetTime) {
943            // Clock rewound.
944            // TODO Randomize??
945            mRawLastResetTime = now;
946        } else {
947            // TODO Do it properly.
948            while ((mRawLastResetTime + mResetInterval) <= now) {
949                mRawLastResetTime += mResetInterval;
950            }
951        }
952        if (prevLastResetTime != mRawLastResetTime) {
953            scheduleSaveBaseState();
954        }
955    }
956
957    /** Return the per-user state. */
958    @GuardedBy("mLock")
959    @NonNull
960    private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) {
961        ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId);
962        if (userPackages == null) {
963            userPackages = loadUserLocked(userId);
964            if (userPackages == null) {
965                userPackages = new ArrayMap<>();
966            }
967            mShortcuts.put(userId, userPackages);
968        }
969        return userPackages;
970    }
971
972    /** Return the per-user per-package state. */
973    @GuardedBy("mLock")
974    @NonNull
975    private PackageShortcuts getPackageShortcutsLocked(
976            @NonNull String packageName, @UserIdInt int userId) {
977        final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId);
978        PackageShortcuts shortcuts = userPackages.get(packageName);
979        if (shortcuts == null) {
980            shortcuts = new PackageShortcuts(userId, packageName);
981            userPackages.put(packageName, shortcuts);
982        }
983        return shortcuts;
984    }
985
986    // === Caller validation ===
987
988    void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
989        if (shortcut.getBitmapPath() != null) {
990            if (DEBUG) {
991                Slog.d(TAG, "Removing " + shortcut.getBitmapPath());
992            }
993            new File(shortcut.getBitmapPath()).delete();
994
995            shortcut.setBitmapPath(null);
996            shortcut.setIconResourceId(0);
997            shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
998        }
999    }
1000
1001    @VisibleForTesting
1002    static class FileOutputStreamWithPath extends FileOutputStream {
1003        private final File mFile;
1004
1005        public FileOutputStreamWithPath(File file) throws FileNotFoundException {
1006            super(file);
1007            mFile = file;
1008        }
1009
1010        public File getFile() {
1011            return mFile;
1012        }
1013    }
1014
1015    /**
1016     * Build the cached bitmap filename for a shortcut icon.
1017     *
1018     * The filename will be based on the ID, except certain characters will be escaped.
1019     */
1020    @VisibleForTesting
1021    FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
1022            throws IOException {
1023        final File packagePath = new File(getUserBitmapFilePath(userId),
1024                shortcut.getPackageName());
1025        if (!packagePath.isDirectory()) {
1026            packagePath.mkdirs();
1027            if (!packagePath.isDirectory()) {
1028                throw new IOException("Unable to create directory " + packagePath);
1029            }
1030            SELinux.restorecon(packagePath);
1031        }
1032
1033        final String baseName = String.valueOf(injectCurrentTimeMillis());
1034        for (int suffix = 0;; suffix++) {
1035            final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
1036            final File file = new File(packagePath, filename);
1037            if (!file.exists()) {
1038                if (DEBUG) {
1039                    Slog.d(TAG, "Saving icon to " + file.getAbsolutePath());
1040                }
1041                return new FileOutputStreamWithPath(file);
1042            }
1043        }
1044    }
1045
1046    void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
1047        if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
1048            return;
1049        }
1050
1051        final long token = Binder.clearCallingIdentity();
1052        try {
1053            // Clear icon info on the shortcut.
1054            shortcut.setIconResourceId(0);
1055            shortcut.setBitmapPath(null);
1056
1057            final Icon icon = shortcut.getIcon();
1058            if (icon == null) {
1059                return; // has no icon
1060            }
1061
1062            Bitmap bitmap = null;
1063            try {
1064                switch (icon.getType()) {
1065                    case Icon.TYPE_RESOURCE: {
1066                        injectValidateIconResPackage(shortcut, icon);
1067
1068                        shortcut.setIconResourceId(icon.getResId());
1069                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
1070                        return;
1071                    }
1072                    case Icon.TYPE_BITMAP: {
1073                        bitmap = icon.getBitmap();
1074                        break;
1075                    }
1076                    case Icon.TYPE_URI: {
1077                        final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId);
1078
1079                        try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
1080
1081                            bitmap = BitmapFactory.decodeStream(is);
1082
1083                        } catch (IOException e) {
1084                            Slog.e(TAG, "Unable to load icon from " + uri);
1085                            return;
1086                        }
1087                        break;
1088                    }
1089                    default:
1090                        // This shouldn't happen because we've already validated the icon, but
1091                        // just in case.
1092                        throw ShortcutInfo.getInvalidIconException();
1093                }
1094                if (bitmap == null) {
1095                    Slog.e(TAG, "Null bitmap detected");
1096                    return;
1097                }
1098                // Shrink and write to the file.
1099                File path = null;
1100                try {
1101                    final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
1102                    try {
1103                        path = out.getFile();
1104
1105                        shrinkBitmap(bitmap, mMaxIconDimension)
1106                                .compress(mIconPersistFormat, mIconPersistQuality, out);
1107
1108                        shortcut.setBitmapPath(out.getFile().getAbsolutePath());
1109                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
1110                    } finally {
1111                        IoUtils.closeQuietly(out);
1112                    }
1113                } catch (IOException|RuntimeException e) {
1114                    // STOPSHIP Change wtf to e
1115                    Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
1116                    if (path != null && path.exists()) {
1117                        path.delete();
1118                    }
1119                }
1120            } finally {
1121                if (bitmap != null) {
1122                    bitmap.recycle();
1123                }
1124                // Once saved, we won't use the original icon information, so null it out.
1125                shortcut.clearIcon();
1126            }
1127        } finally {
1128            Binder.restoreCallingIdentity(token);
1129        }
1130    }
1131
1132    // Unfortunately we can't do this check in unit tests because we fake creator package names,
1133    // so override in unit tests.
1134    // TODO CTS this case.
1135    void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
1136        if (!shortcut.getPackageName().equals(icon.getResPackage())) {
1137            throw new IllegalArgumentException(
1138                    "Icon resource must reside in shortcut owner package");
1139        }
1140    }
1141
1142    @VisibleForTesting
1143    static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
1144        // Original width/height.
1145        final int ow = in.getWidth();
1146        final int oh = in.getHeight();
1147        if ((ow <= maxSize) && (oh <= maxSize)) {
1148            if (DEBUG) {
1149                Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh));
1150            }
1151            return in;
1152        }
1153        final int longerDimension = Math.max(ow, oh);
1154
1155        // New width and height.
1156        final int nw = ow * maxSize / longerDimension;
1157        final int nh = oh * maxSize / longerDimension;
1158        if (DEBUG) {
1159            Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d",
1160                    ow, oh, nw, nh));
1161        }
1162
1163        final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888);
1164        final Canvas c = new Canvas(scaledBitmap);
1165
1166        final RectF dst = new RectF(0, 0, nw, nh);
1167
1168        c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null);
1169
1170        in.recycle();
1171
1172        return scaledBitmap;
1173    }
1174
1175    // === Caller validation ===
1176
1177    private boolean isCallerSystem() {
1178        final int callingUid = injectBinderCallingUid();
1179         return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
1180    }
1181
1182    private boolean isCallerShell() {
1183        final int callingUid = injectBinderCallingUid();
1184        return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
1185    }
1186
1187    private void enforceSystemOrShell() {
1188        Preconditions.checkState(isCallerSystem() || isCallerShell(),
1189                "Caller must be system or shell");
1190    }
1191
1192    private void enforceShell() {
1193        Preconditions.checkState(isCallerShell(), "Caller must be shell");
1194    }
1195
1196    private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) {
1197        Preconditions.checkStringNotEmpty(packageName, "packageName");
1198
1199        if (isCallerSystem()) {
1200            return; // no check
1201        }
1202
1203        final int callingUid = injectBinderCallingUid();
1204
1205        // Otherwise, make sure the arguments are valid.
1206        if (UserHandle.getUserId(callingUid) != userId) {
1207            throw new SecurityException("Invalid user-ID");
1208        }
1209        if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
1210            return; // Caller is valid.
1211        }
1212        throw new SecurityException("Caller UID= doesn't own " + packageName);
1213    }
1214
1215    // Test overrides it.
1216    int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
1217        try {
1218
1219            // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info?
1220
1221            return mContext.getPackageManager().getPackageUidAsUser(packageName,
1222                    PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE
1223                            | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
1224        } catch (NameNotFoundException e) {
1225            return -1;
1226        }
1227    }
1228
1229    /**
1230     * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
1231     */
1232    void enforceMaxDynamicShortcuts(int numShortcuts) {
1233        if (numShortcuts > mMaxDynamicShortcuts) {
1234            throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
1235        }
1236    }
1237
1238    /**
1239     * - Sends a notification to LauncherApps
1240     * - Write to file
1241     */
1242    private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) {
1243        notifyListeners(packageName, userId);
1244        scheduleSaveUser(userId);
1245    }
1246
1247    private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) {
1248        final ArrayList<ShortcutChangeListener> copy;
1249        final List<ShortcutInfo> shortcuts = new ArrayList<>();
1250        synchronized (mLock) {
1251            copy = new ArrayList<>(mListeners);
1252
1253            getPackageShortcutsLocked(packageName, userId)
1254                    .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
1255        }
1256        for (int i = copy.size() - 1; i >= 0; i--) {
1257            copy.get(i).onShortcutChanged(packageName, shortcuts, userId);
1258        }
1259    }
1260
1261    /**
1262     * Clean up / validate an incoming shortcut.
1263     * - Make sure all mandatory fields are set.
1264     * - Make sure the intent's extras are persistable, and them to set
1265     *  {@link ShortcutInfo#mIntentPersistableExtras}.  Also clear its extras.
1266     * - Clear flags.
1267     *
1268     * TODO Detailed unit tests
1269     */
1270    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
1271        Preconditions.checkNotNull(shortcut, "Null shortcut detected");
1272        if (shortcut.getActivityComponent() != null) {
1273            Preconditions.checkState(
1274                    shortcut.getPackageName().equals(
1275                            shortcut.getActivityComponent().getPackageName()),
1276                    "Activity package name mismatch");
1277        }
1278
1279        if (!forUpdate) {
1280            shortcut.enforceMandatoryFields();
1281        }
1282        if (shortcut.getIcon() != null) {
1283            ShortcutInfo.validateIcon(shortcut.getIcon());
1284        }
1285
1286        validateForXml(shortcut.getId());
1287        validateForXml(shortcut.getTitle());
1288        validatePersistableBundleForXml(shortcut.getIntentPersistableExtras());
1289        validatePersistableBundleForXml(shortcut.getExtras());
1290
1291        shortcut.setFlags(0);
1292    }
1293
1294    // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those
1295    // characters.
1296
1297    private static void validatePersistableBundleForXml(PersistableBundle b) {
1298        if (b == null || b.size() == 0) {
1299            return;
1300        }
1301        for (String key : b.keySet()) {
1302            validateForXml(key);
1303            final Object value = b.get(key);
1304            if (value == null) {
1305                continue;
1306            } else if (value instanceof String) {
1307                validateForXml((String) value);
1308            } else if (value instanceof String[]) {
1309                for (String v : (String[]) value) {
1310                    validateForXml(v);
1311                }
1312            } else if (value instanceof PersistableBundle) {
1313                validatePersistableBundleForXml((PersistableBundle) value);
1314            }
1315        }
1316    }
1317
1318    private static void validateForXml(String s) {
1319        if (TextUtils.isEmpty(s)) {
1320            return;
1321        }
1322        for (int i = s.length() - 1; i >= 0; i--) {
1323            if (!isAllowedInXml(s.charAt(i))) {
1324                throw new IllegalArgumentException("Unsupported character detected in: " + s);
1325            }
1326        }
1327    }
1328
1329    private static boolean isAllowedInXml(char c) {
1330        return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
1331    }
1332
1333    // === APIs ===
1334
1335    @Override
1336    public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1337            @UserIdInt int userId) {
1338        verifyCaller(packageName, userId);
1339
1340        final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1341        final int size = newShortcuts.size();
1342
1343        synchronized (mLock) {
1344            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1345
1346            // Throttling.
1347            if (!ps.tryApiCall(this)) {
1348                return false;
1349            }
1350            enforceMaxDynamicShortcuts(size);
1351
1352            // Validate the shortcuts.
1353            for (int i = 0; i < size; i++) {
1354                fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
1355            }
1356
1357            // First, remove all un-pinned; dynamic shortcuts
1358            ps.deleteAllDynamicShortcuts(this);
1359
1360            // Then, add/update all.  We need to make sure to take over "pinned" flag.
1361            for (int i = 0; i < size; i++) {
1362                final ShortcutInfo newShortcut = newShortcuts.get(i);
1363                newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1364                ps.updateShortcutWithCapping(this, newShortcut);
1365            }
1366        }
1367        userPackageChanged(packageName, userId);
1368        return true;
1369    }
1370
1371    @Override
1372    public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1373            @UserIdInt int userId) {
1374        verifyCaller(packageName, userId);
1375
1376        final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1377        final int size = newShortcuts.size();
1378
1379        synchronized (mLock) {
1380            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1381
1382            // Throttling.
1383            if (!ps.tryApiCall(this)) {
1384                return false;
1385            }
1386
1387            for (int i = 0; i < size; i++) {
1388                final ShortcutInfo source = newShortcuts.get(i);
1389                fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
1390
1391                final ShortcutInfo target = ps.findShortcutById(source.getId());
1392                if (target != null) {
1393                    final boolean replacingIcon = (source.getIcon() != null);
1394                    if (replacingIcon) {
1395                        removeIcon(userId, target);
1396                    }
1397
1398                    target.copyNonNullFieldsFrom(source);
1399
1400                    if (replacingIcon) {
1401                        saveIconAndFixUpShortcut(userId, target);
1402                    }
1403                }
1404            }
1405        }
1406        userPackageChanged(packageName, userId);
1407
1408        return true;
1409    }
1410
1411    @Override
1412    public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut,
1413            @UserIdInt int userId) {
1414        verifyCaller(packageName, userId);
1415
1416        synchronized (mLock) {
1417            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1418
1419            // Throttling.
1420            if (!ps.tryApiCall(this)) {
1421                return false;
1422            }
1423
1424            // Validate the shortcut.
1425            fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
1426
1427            // Add it.
1428            newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1429            ps.updateShortcutWithCapping(this, newShortcut);
1430        }
1431        userPackageChanged(packageName, userId);
1432
1433        return true;
1434    }
1435
1436    @Override
1437    public void deleteDynamicShortcut(String packageName, String shortcutId,
1438            @UserIdInt int userId) {
1439        verifyCaller(packageName, userId);
1440        Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
1441
1442        synchronized (mLock) {
1443            getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
1444        }
1445        userPackageChanged(packageName, userId);
1446    }
1447
1448    @Override
1449    public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) {
1450        verifyCaller(packageName, userId);
1451
1452        synchronized (mLock) {
1453            getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
1454        }
1455        userPackageChanged(packageName, userId);
1456    }
1457
1458    @Override
1459    public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName,
1460            @UserIdInt int userId) {
1461        verifyCaller(packageName, userId);
1462        synchronized (mLock) {
1463            return getShortcutsWithQueryLocked(
1464                    packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1465                    ShortcutInfo::isDynamic);
1466        }
1467    }
1468
1469    @Override
1470    public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName,
1471            @UserIdInt int userId) {
1472        verifyCaller(packageName, userId);
1473        synchronized (mLock) {
1474            return getShortcutsWithQueryLocked(
1475                    packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1476                    ShortcutInfo::isPinned);
1477        }
1478    }
1479
1480    private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName,
1481            @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) {
1482
1483        final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1484
1485        getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags);
1486
1487        return new ParceledListSlice<>(ret);
1488    }
1489
1490    @Override
1491    public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId)
1492            throws RemoteException {
1493        verifyCaller(packageName, userId);
1494
1495        return mMaxDynamicShortcuts;
1496    }
1497
1498    @Override
1499    public int getRemainingCallCount(String packageName, @UserIdInt int userId) {
1500        verifyCaller(packageName, userId);
1501
1502        synchronized (mLock) {
1503            return mMaxDailyUpdates
1504                    - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
1505        }
1506    }
1507
1508    @Override
1509    public long getRateLimitResetTime(String packageName, @UserIdInt int userId) {
1510        verifyCaller(packageName, userId);
1511
1512        synchronized (mLock) {
1513            return getNextResetTimeLocked();
1514        }
1515    }
1516
1517    @Override
1518    public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
1519        synchronized (mLock) {
1520            return mMaxIconDimension;
1521        }
1522    }
1523
1524    /**
1525     * Reset all throttling, for developer options and command line.  Only system/shell can call it.
1526     */
1527    @Override
1528    public void resetThrottling() {
1529        enforceSystemOrShell();
1530
1531        resetThrottlingInner();
1532    }
1533
1534    @VisibleForTesting
1535    void resetThrottlingInner() {
1536        synchronized (mLock) {
1537            mRawLastResetTime = injectCurrentTimeMillis();
1538        }
1539        scheduleSaveBaseState();
1540        Slog.i(TAG, "ShortcutManager: throttling counter reset");
1541    }
1542
1543    /**
1544     * Entry point from {@link LauncherApps}.
1545     */
1546    private class LocalService extends ShortcutServiceInternal {
1547        @Override
1548        public List<ShortcutInfo> getShortcuts(
1549                @NonNull String callingPackage, long changedSince,
1550                @Nullable String packageName, @Nullable ComponentName componentName,
1551                int queryFlags, int userId) {
1552            final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1553            final int cloneFlag =
1554                    ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0)
1555                            ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER
1556                            : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO;
1557
1558            synchronized (mLock) {
1559                if (packageName != null) {
1560                    getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags,
1561                            userId, ret, cloneFlag);
1562                } else {
1563                    final ArrayMap<String, PackageShortcuts> packages =
1564                            getUserShortcutsLocked(userId);
1565                    for (int i = packages.size() - 1; i >= 0; i--) {
1566                        getShortcutsInnerLocked(
1567                                packages.keyAt(i),
1568                                changedSince, componentName, queryFlags, userId, ret, cloneFlag);
1569                    }
1570                }
1571            }
1572            return ret;
1573        }
1574
1575        private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince,
1576                @Nullable ComponentName componentName, int queryFlags,
1577                int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) {
1578            getPackageShortcutsLocked(packageName, userId).findAll(ret,
1579                    (ShortcutInfo si) -> {
1580                        if (si.getLastChangedTimestamp() < changedSince) {
1581                            return false;
1582                        }
1583                        if (componentName != null
1584                                && !componentName.equals(si.getActivityComponent())) {
1585                            return false;
1586                        }
1587                        final boolean matchDynamic =
1588                                ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0)
1589                                && si.isDynamic();
1590                        final boolean matchPinned =
1591                                ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0)
1592                                        && si.isPinned();
1593                        return matchDynamic || matchPinned;
1594                    }, cloneFlag);
1595        }
1596
1597        @Override
1598        public List<ShortcutInfo> getShortcutInfo(
1599                @NonNull String callingPackage,
1600                @NonNull String packageName, @Nullable List<String> ids, int userId) {
1601            // Calling permission must be checked by LauncherAppsImpl.
1602            Preconditions.checkStringNotEmpty(packageName, "packageName");
1603
1604            final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size());
1605            final ArraySet<String> idSet = new ArraySet<>(ids);
1606            synchronized (mLock) {
1607                getPackageShortcutsLocked(packageName, userId).findAll(ret,
1608                        (ShortcutInfo si) -> idSet.contains(si.getId()),
1609                        ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
1610            }
1611            return ret;
1612        }
1613
1614        @Override
1615        public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName,
1616                @NonNull List<String> shortcutIds, int userId) {
1617            // Calling permission must be checked by LauncherAppsImpl.
1618            Preconditions.checkStringNotEmpty(packageName, "packageName");
1619            Preconditions.checkNotNull(shortcutIds, "shortcutIds");
1620
1621            synchronized (mLock) {
1622                getPackageShortcutsLocked(packageName, userId).replacePinned(
1623                        ShortcutService.this, callingPackage, shortcutIds);
1624            }
1625            userPackageChanged(packageName, userId);
1626        }
1627
1628        @Override
1629        public Intent createShortcutIntent(@NonNull String callingPackage,
1630                @NonNull String packageName, @NonNull String shortcutId, int userId) {
1631            // Calling permission must be checked by LauncherAppsImpl.
1632            Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty");
1633            Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty");
1634
1635            synchronized (mLock) {
1636                final ShortcutInfo fullShortcut =
1637                        getPackageShortcutsLocked(packageName, userId)
1638                        .findShortcutById(shortcutId);
1639                return fullShortcut == null ? null : fullShortcut.getIntent();
1640            }
1641        }
1642
1643        @Override
1644        public void addListener(@NonNull ShortcutChangeListener listener) {
1645            synchronized (mLock) {
1646                mListeners.add(Preconditions.checkNotNull(listener));
1647            }
1648        }
1649
1650        @Override
1651        public int getShortcutIconResId(@NonNull String callingPackage,
1652                @NonNull ShortcutInfo shortcut, int userId) {
1653            Preconditions.checkNotNull(shortcut, "shortcut");
1654
1655            synchronized (mLock) {
1656                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1657                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1658                return (shortcutInfo != null && shortcutInfo.hasIconResource())
1659                        ? shortcutInfo.getIconResourceId() : 0;
1660            }
1661        }
1662
1663        @Override
1664        public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
1665                @NonNull ShortcutInfo shortcut, int userId) {
1666            Preconditions.checkNotNull(shortcut, "shortcut");
1667
1668            synchronized (mLock) {
1669                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1670                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1671                if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
1672                    return null;
1673                }
1674                try {
1675                    return ParcelFileDescriptor.open(
1676                            new File(shortcutInfo.getBitmapPath()),
1677                            ParcelFileDescriptor.MODE_READ_ONLY);
1678                } catch (FileNotFoundException e) {
1679                    Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
1680                    return null;
1681                }
1682            }
1683        }
1684    }
1685
1686    // === Dump ===
1687
1688    @Override
1689    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1690        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
1691                != PackageManager.PERMISSION_GRANTED) {
1692            pw.println("Permission Denial: can't dump UserManager from from pid="
1693                    + Binder.getCallingPid()
1694                    + ", uid=" + Binder.getCallingUid()
1695                    + " without permission "
1696                    + android.Manifest.permission.DUMP);
1697            return;
1698        }
1699        dumpInner(pw);
1700    }
1701
1702    @VisibleForTesting
1703    void dumpInner(PrintWriter pw) {
1704        synchronized (mLock) {
1705            final long now = injectCurrentTimeMillis();
1706            pw.print("Now: [");
1707            pw.print(now);
1708            pw.print("] ");
1709            pw.print(formatTime(now));
1710
1711            pw.print("  Raw last reset: [");
1712            pw.print(mRawLastResetTime);
1713            pw.print("] ");
1714            pw.print(formatTime(mRawLastResetTime));
1715
1716            final long last = getLastResetTimeLocked();
1717            pw.print("  Last reset: [");
1718            pw.print(last);
1719            pw.print("] ");
1720            pw.print(formatTime(last));
1721
1722            final long next = getNextResetTimeLocked();
1723            pw.print("  Next reset: [");
1724            pw.print(next);
1725            pw.print("] ");
1726            pw.print(formatTime(next));
1727            pw.println();
1728
1729            pw.print("  Max icon dim: ");
1730            pw.print(mMaxIconDimension);
1731            pw.print("  Icon format: ");
1732            pw.print(mIconPersistFormat);
1733            pw.print("  Icon quality: ");
1734            pw.print(mIconPersistQuality);
1735            pw.println();
1736
1737            pw.println();
1738
1739            for (int i = 0; i < mShortcuts.size(); i++) {
1740                dumpUserLocked(pw, mShortcuts.keyAt(i));
1741            }
1742        }
1743    }
1744
1745    private void dumpUserLocked(PrintWriter pw, int userId) {
1746        pw.print("  User: ");
1747        pw.print(userId);
1748        pw.println();
1749
1750        final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId);
1751        if (packages == null) {
1752            return;
1753        }
1754        for (int j = 0; j < packages.size(); j++) {
1755            dumpPackageLocked(pw, userId, packages.keyAt(j));
1756        }
1757        pw.println();
1758    }
1759
1760    private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) {
1761        final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName);
1762        if (packageShortcuts == null) {
1763            return;
1764        }
1765
1766        pw.print("    Package: ");
1767        pw.print(packageName);
1768        pw.println();
1769
1770        pw.print("      Calls: ");
1771        pw.print(packageShortcuts.getApiCallCount(this));
1772        pw.println();
1773
1774        // This should be after getApiCallCount(), which may update it.
1775        pw.print("      Last reset: [");
1776        pw.print(packageShortcuts.mLastResetTime);
1777        pw.print("] ");
1778        pw.print(formatTime(packageShortcuts.mLastResetTime));
1779        pw.println();
1780
1781        pw.println("      Shortcuts:");
1782        long totalBitmapSize = 0;
1783        final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
1784        final int size = shortcuts.size();
1785        for (int i = 0; i < size; i++) {
1786            final ShortcutInfo si = shortcuts.valueAt(i);
1787            pw.print("        ");
1788            pw.println(si.toInsecureString());
1789            if (si.hasIconFile()) {
1790                final long len = new File(si.getBitmapPath()).length();
1791                pw.print("          ");
1792                pw.print("bitmap size=");
1793                pw.println(len);
1794
1795                totalBitmapSize += len;
1796            }
1797        }
1798        pw.print("      Total bitmap size: ");
1799        pw.print(totalBitmapSize);
1800        pw.print(" (");
1801        pw.print(Formatter.formatFileSize(mContext, totalBitmapSize));
1802        pw.println(")");
1803    }
1804
1805    private static String formatTime(long time) {
1806        Time tobj = new Time();
1807        tobj.set(time);
1808        return tobj.format("%Y-%m-%d %H:%M:%S");
1809    }
1810
1811    // === Shell support ===
1812
1813    @Override
1814    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
1815            String[] args, ResultReceiver resultReceiver) throws RemoteException {
1816
1817        enforceShell();
1818
1819        (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
1820    }
1821
1822    /**
1823     * Handle "adb shell cmd".
1824     */
1825    private class MyShellCommand extends ShellCommand {
1826        @Override
1827        public int onCommand(String cmd) {
1828            if (cmd == null) {
1829                return handleDefaultCommands(cmd);
1830            }
1831            final PrintWriter pw = getOutPrintWriter();
1832            switch(cmd) {
1833                case "reset-package-throttling":
1834                    return handleResetPackageThrottling();
1835                case "reset-throttling":
1836                    return handleResetThrottling();
1837                default:
1838                    return handleDefaultCommands(cmd);
1839            }
1840        }
1841
1842        @Override
1843        public void onHelp() {
1844            final PrintWriter pw = getOutPrintWriter();
1845            pw.println("Usage: cmd shortcut COMMAND [options ...]");
1846            pw.println();
1847            pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE");
1848            pw.println("    Reset throttling for a package");
1849            pw.println();
1850            pw.println("cmd shortcut reset-throttling");
1851            pw.println("    Reset throttling for all packages and users");
1852            pw.println();
1853        }
1854
1855        private int handleResetThrottling() {
1856            resetThrottling();
1857            return 0;
1858        }
1859
1860        private int handleResetPackageThrottling() {
1861            final PrintWriter pw = getOutPrintWriter();
1862
1863            int userId = UserHandle.USER_SYSTEM;
1864            String opt;
1865            while ((opt = getNextOption()) != null) {
1866                switch (opt) {
1867                    case "--user":
1868                        userId = UserHandle.parseUserArg(getNextArgRequired());
1869                        break;
1870                    default:
1871                        pw.println("Error: Unknown option: " + opt);
1872                        return 1;
1873                }
1874            }
1875            final String packageName = getNextArgRequired();
1876
1877            synchronized (mLock) {
1878                getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine();
1879                saveUserLocked(userId);
1880            }
1881
1882            return 0;
1883        }
1884    }
1885
1886    // === Unit test support ===
1887
1888    // Injection point.
1889    long injectCurrentTimeMillis() {
1890        return System.currentTimeMillis();
1891    }
1892
1893    // Injection point.
1894    int injectBinderCallingUid() {
1895        return getCallingUid();
1896    }
1897
1898    File injectSystemDataPath() {
1899        return Environment.getDataSystemDirectory();
1900    }
1901
1902    File injectUserDataPath(@UserIdInt int userId) {
1903        return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
1904    }
1905
1906    boolean injectIsLowRamDevice() {
1907        return ActivityManager.isLowRamDeviceStatic();
1908    }
1909
1910    File getUserBitmapFilePath(@UserIdInt int userId) {
1911        return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
1912    }
1913
1914    @VisibleForTesting
1915    SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() {
1916        return mShortcuts;
1917    }
1918
1919    @VisibleForTesting
1920    void setMaxDynamicShortcutsForTest(int max) {
1921        mMaxDynamicShortcuts = max;
1922    }
1923
1924    @VisibleForTesting
1925    void setMaxDailyUpdatesForTest(int max) {
1926        mMaxDailyUpdates = max;
1927    }
1928
1929    @VisibleForTesting
1930    void setMaxIconDimensionForTest(int dimension) {
1931        mMaxIconDimension = dimension;
1932    }
1933
1934    @VisibleForTesting
1935    public void setResetIntervalForTest(long interval) {
1936        mResetInterval = interval;
1937    }
1938}
1939