ShortcutUser.java revision 20b8221c9fd279d3cc48ef876a7fbe5fff29ef67
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.server.pm;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.annotation.UserIdInt;
21import android.content.ComponentName;
22import android.content.pm.ShortcutManager;
23import android.text.TextUtils;
24import android.text.format.Formatter;
25import android.util.ArrayMap;
26import android.util.Log;
27import android.util.Slog;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.util.Preconditions;
31import com.android.server.pm.ShortcutService.DumpFilter;
32import com.android.server.pm.ShortcutService.InvalidFileFormatException;
33
34import libcore.util.Objects;
35
36import org.json.JSONArray;
37import org.json.JSONException;
38import org.json.JSONObject;
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlPullParserException;
41import org.xmlpull.v1.XmlSerializer;
42
43import java.io.File;
44import java.io.IOException;
45import java.io.PrintWriter;
46import java.util.function.Consumer;
47
48/**
49 * User information used by {@link ShortcutService}.
50 *
51 * All methods should be guarded by {@code #mService.mLock}.
52 */
53class ShortcutUser {
54    private static final String TAG = ShortcutService.TAG;
55
56    static final String TAG_ROOT = "user";
57    private static final String TAG_LAUNCHER = "launcher";
58
59    private static final String ATTR_VALUE = "value";
60    private static final String ATTR_KNOWN_LOCALES = "locales";
61
62    // Suffix "2" was added to force rescan all packages after the next OTA.
63    private static final String ATTR_LAST_APP_SCAN_TIME = "last-app-scan-time2";
64    private static final String ATTR_LAST_APP_SCAN_OS_FINGERPRINT = "last-app-scan-fp";
65    private static final String KEY_USER_ID = "userId";
66    private static final String KEY_LAUNCHERS = "launchers";
67    private static final String KEY_PACKAGES = "packages";
68
69    static final class PackageWithUser {
70        final int userId;
71        final String packageName;
72
73        private PackageWithUser(int userId, String packageName) {
74            this.userId = userId;
75            this.packageName = Preconditions.checkNotNull(packageName);
76        }
77
78        public static PackageWithUser of(int userId, String packageName) {
79            return new PackageWithUser(userId, packageName);
80        }
81
82        public static PackageWithUser of(ShortcutPackageItem spi) {
83            return new PackageWithUser(spi.getPackageUserId(), spi.getPackageName());
84        }
85
86        @Override
87        public int hashCode() {
88            return packageName.hashCode() ^ userId;
89        }
90
91        @Override
92        public boolean equals(Object obj) {
93            if (!(obj instanceof PackageWithUser)) {
94                return false;
95            }
96            final PackageWithUser that = (PackageWithUser) obj;
97
98            return userId == that.userId && packageName.equals(that.packageName);
99        }
100
101        @Override
102        public String toString() {
103            return String.format("[Package: %d, %s]", userId, packageName);
104        }
105    }
106
107    final ShortcutService mService;
108
109    @UserIdInt
110    private final int mUserId;
111
112    private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
113
114    private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
115
116    /**
117     * Last known launcher.  It's used when the default launcher isn't set in PM -- i.e.
118     * when getHomeActivitiesAsUser() return null.  We need it so that in this situation the
119     * previously default launcher can still access shortcuts.
120     */
121    private ComponentName mLastKnownLauncher;
122
123    /** In-memory-cached default launcher. */
124    private ComponentName mCachedLauncher;
125
126    private String mKnownLocales;
127
128    private long mLastAppScanTime;
129
130    private String mLastAppScanOsFingerprint;
131
132    public ShortcutUser(ShortcutService service, int userId) {
133        mService = service;
134        mUserId = userId;
135    }
136
137    public int getUserId() {
138        return mUserId;
139    }
140
141    public long getLastAppScanTime() {
142        return mLastAppScanTime;
143    }
144
145    public void setLastAppScanTime(long lastAppScanTime) {
146        mLastAppScanTime = lastAppScanTime;
147    }
148
149    public String getLastAppScanOsFingerprint() {
150        return mLastAppScanOsFingerprint;
151    }
152
153    public void setLastAppScanOsFingerprint(String lastAppScanOsFingerprint) {
154        mLastAppScanOsFingerprint = lastAppScanOsFingerprint;
155    }
156
157    // We don't expose this directly to non-test code because only ShortcutUser should add to/
158    // remove from it.
159    @VisibleForTesting
160    ArrayMap<String, ShortcutPackage> getAllPackagesForTest() {
161        return mPackages;
162    }
163
164    public boolean hasPackage(@NonNull String packageName) {
165        return mPackages.containsKey(packageName);
166    }
167
168    private void addPackage(@NonNull ShortcutPackage p) {
169        p.replaceUser(this);
170        mPackages.put(p.getPackageName(), p);
171    }
172
173    public ShortcutPackage removePackage(@NonNull String packageName) {
174        final ShortcutPackage removed = mPackages.remove(packageName);
175
176        mService.cleanupBitmapsForPackage(mUserId, packageName);
177
178        return removed;
179    }
180
181    // We don't expose this directly to non-test code because only ShortcutUser should add to/
182    // remove from it.
183    @VisibleForTesting
184    ArrayMap<PackageWithUser, ShortcutLauncher> getAllLaunchersForTest() {
185        return mLaunchers;
186    }
187
188    private void addLauncher(ShortcutLauncher launcher) {
189        launcher.replaceUser(this);
190        mLaunchers.put(PackageWithUser.of(launcher.getPackageUserId(),
191                launcher.getPackageName()), launcher);
192    }
193
194    @Nullable
195    public ShortcutLauncher removeLauncher(
196            @UserIdInt int packageUserId, @NonNull String packageName) {
197        return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName));
198    }
199
200    @Nullable
201    public ShortcutPackage getPackageShortcutsIfExists(@NonNull String packageName) {
202        final ShortcutPackage ret = mPackages.get(packageName);
203        if (ret != null) {
204            ret.attemptToRestoreIfNeededAndSave();
205        }
206        return ret;
207    }
208
209    @NonNull
210    public ShortcutPackage getPackageShortcuts(@NonNull String packageName) {
211        ShortcutPackage ret = getPackageShortcutsIfExists(packageName);
212        if (ret == null) {
213            ret = new ShortcutPackage(this, mUserId, packageName);
214            mPackages.put(packageName, ret);
215        }
216        return ret;
217    }
218
219    @NonNull
220    public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName,
221            @UserIdInt int launcherUserId) {
222        final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName);
223        ShortcutLauncher ret = mLaunchers.get(key);
224        if (ret == null) {
225            ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
226            mLaunchers.put(key, ret);
227        } else {
228            ret.attemptToRestoreIfNeededAndSave();
229        }
230        return ret;
231    }
232
233    public void forAllPackages(Consumer<? super ShortcutPackage> callback) {
234        final int size = mPackages.size();
235        for (int i = 0; i < size; i++) {
236            callback.accept(mPackages.valueAt(i));
237        }
238    }
239
240    public void forAllLaunchers(Consumer<? super ShortcutLauncher> callback) {
241        final int size = mLaunchers.size();
242        for (int i = 0; i < size; i++) {
243            callback.accept(mLaunchers.valueAt(i));
244        }
245    }
246
247    public void forAllPackageItems(Consumer<? super ShortcutPackageItem> callback) {
248        forAllLaunchers(callback);
249        forAllPackages(callback);
250    }
251
252    public void forPackageItem(@NonNull String packageName, @UserIdInt int packageUserId,
253            Consumer<ShortcutPackageItem> callback) {
254        forAllPackageItems(spi -> {
255            if ((spi.getPackageUserId() == packageUserId)
256                    && spi.getPackageName().equals(packageName)) {
257                callback.accept(spi);
258            }
259        });
260    }
261
262    /**
263     * Must be called at any entry points on {@link ShortcutManager} APIs to make sure the
264     * information on the package is up-to-date.
265     *
266     * We use broadcasts to handle locale changes and package changes, but because broadcasts
267     * are asynchronous, there's a chance a publisher calls getXxxShortcuts() after a certain event
268     * (e.g. system locale change) but shortcut manager hasn't finished processing the broadcast.
269     *
270     * So we call this method at all entry points from publishers to make sure we update all
271     * relevant information.
272     *
273     * Similar inconsistencies can happen when the launcher fetches shortcut information, but
274     * that's a less of an issue because for the launcher we report shortcut changes with
275     * callbacks.
276     */
277    public void onCalledByPublisher(@NonNull String packageName) {
278        detectLocaleChange();
279        rescanPackageIfNeeded(packageName, /*forceRescan=*/ false);
280    }
281
282    private String getKnownLocales() {
283        if (TextUtils.isEmpty(mKnownLocales)) {
284            mKnownLocales = mService.injectGetLocaleTagsForUser(mUserId);
285            mService.scheduleSaveUser(mUserId);
286        }
287        return mKnownLocales;
288    }
289
290    /**
291     * Check to see if the system locale has changed, and if so, reset throttling
292     * and update resource strings.
293     */
294    public void detectLocaleChange() {
295        final String currentLocales = mService.injectGetLocaleTagsForUser(mUserId);
296        if (getKnownLocales().equals(currentLocales)) {
297            return;
298        }
299        if (ShortcutService.DEBUG) {
300            Slog.d(TAG, "Locale changed from " + currentLocales + " to " + mKnownLocales
301                    + " for user " + mUserId);
302        }
303        mKnownLocales = currentLocales;
304
305        forAllPackages(pkg -> {
306            pkg.resetRateLimiting();
307            pkg.resolveResourceStrings();
308        });
309
310        mService.scheduleSaveUser(mUserId);
311    }
312
313    public void rescanPackageIfNeeded(@NonNull String packageName, boolean forceRescan) {
314        final boolean isNewApp = !mPackages.containsKey(packageName);
315
316        final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
317
318        if (!shortcutPackage.rescanPackageIfNeeded(isNewApp, forceRescan)) {
319            if (isNewApp) {
320                mPackages.remove(packageName);
321            }
322        }
323    }
324
325    public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName,
326            @UserIdInt int packageUserId) {
327        forPackageItem(packageName, packageUserId, spi -> {
328            spi.attemptToRestoreIfNeededAndSave();
329        });
330    }
331
332    public void saveToXml(XmlSerializer out, boolean forBackup)
333            throws IOException, XmlPullParserException {
334        out.startTag(null, TAG_ROOT);
335
336        if (!forBackup) {
337            // Don't have to back them up.
338            ShortcutService.writeAttr(out, ATTR_KNOWN_LOCALES, mKnownLocales);
339            ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_TIME,
340                    mLastAppScanTime);
341            ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_OS_FINGERPRINT,
342                    mLastAppScanOsFingerprint);
343
344            ShortcutService.writeTagValue(out, TAG_LAUNCHER, mLastKnownLauncher);
345        }
346
347        // Can't use forEachPackageItem due to the checked exceptions.
348        {
349            final int size = mLaunchers.size();
350            for (int i = 0; i < size; i++) {
351                saveShortcutPackageItem(out, mLaunchers.valueAt(i), forBackup);
352            }
353        }
354        {
355            final int size = mPackages.size();
356            for (int i = 0; i < size; i++) {
357                saveShortcutPackageItem(out, mPackages.valueAt(i), forBackup);
358            }
359        }
360
361        out.endTag(null, TAG_ROOT);
362    }
363
364    private void saveShortcutPackageItem(XmlSerializer out,
365            ShortcutPackageItem spi, boolean forBackup) throws IOException, XmlPullParserException {
366        if (forBackup) {
367            if (!mService.shouldBackupApp(spi.getPackageName(), spi.getPackageUserId())) {
368                return; // Don't save.
369            }
370            if (spi.getPackageUserId() != spi.getOwnerUserId()) {
371                return; // Don't save cross-user information.
372            }
373        }
374        spi.saveToXml(out, forBackup);
375    }
376
377    public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId,
378            boolean fromBackup) throws IOException, XmlPullParserException, InvalidFileFormatException {
379        final ShortcutUser ret = new ShortcutUser(s, userId);
380
381        try {
382            ret.mKnownLocales = ShortcutService.parseStringAttribute(parser,
383                    ATTR_KNOWN_LOCALES);
384
385            // If lastAppScanTime is in the future, that means the clock went backwards.
386            // Just scan all apps again.
387            final long lastAppScanTime = ShortcutService.parseLongAttribute(parser,
388                    ATTR_LAST_APP_SCAN_TIME);
389            final long currentTime = s.injectCurrentTimeMillis();
390            ret.mLastAppScanTime = lastAppScanTime < currentTime ? lastAppScanTime : 0;
391            ret.mLastAppScanOsFingerprint = ShortcutService.parseStringAttribute(parser,
392                    ATTR_LAST_APP_SCAN_OS_FINGERPRINT);
393            final int outerDepth = parser.getDepth();
394            int type;
395            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
396                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
397                if (type != XmlPullParser.START_TAG) {
398                    continue;
399                }
400                final int depth = parser.getDepth();
401                final String tag = parser.getName();
402
403                if (depth == outerDepth + 1) {
404                    switch (tag) {
405                        case TAG_LAUNCHER: {
406                            ret.mLastKnownLauncher = ShortcutService.parseComponentNameAttribute(
407                                    parser, ATTR_VALUE);
408                            continue;
409                        }
410                        case ShortcutPackage.TAG_ROOT: {
411                            final ShortcutPackage shortcuts = ShortcutPackage.loadFromXml(
412                                    s, ret, parser, fromBackup);
413
414                            // Don't use addShortcut(), we don't need to save the icon.
415                            ret.mPackages.put(shortcuts.getPackageName(), shortcuts);
416                            continue;
417                        }
418
419                        case ShortcutLauncher.TAG_ROOT: {
420                            ret.addLauncher(
421                                    ShortcutLauncher.loadFromXml(parser, ret, userId, fromBackup));
422                            continue;
423                        }
424                    }
425                }
426                ShortcutService.warnForInvalidTag(depth, tag);
427            }
428        } catch (RuntimeException e) {
429            throw new ShortcutService.InvalidFileFormatException(
430                    "Unable to parse file", e);
431        }
432        return ret;
433    }
434
435    public ComponentName getLastKnownLauncher() {
436        return mLastKnownLauncher;
437    }
438
439    public void setLauncher(ComponentName launcherComponent) {
440        setLauncher(launcherComponent, /* allowPurgeLastKnown */ false);
441    }
442
443    /** Clears the launcher information without clearing the last known one */
444    public void clearLauncher() {
445        setLauncher(null);
446    }
447
448    /**
449     * Clears the launcher information *with(* clearing the last known one; we do this witl
450     * "cmd shortcut clear-default-launcher".
451     */
452    public void forceClearLauncher() {
453        setLauncher(null, /* allowPurgeLastKnown */ true);
454    }
455
456    private void setLauncher(ComponentName launcherComponent, boolean allowPurgeLastKnown) {
457        mCachedLauncher = launcherComponent; // Always update the in-memory cache.
458
459        if (Objects.equal(mLastKnownLauncher, launcherComponent)) {
460            return;
461        }
462        if (!allowPurgeLastKnown && launcherComponent == null) {
463            return;
464        }
465        mLastKnownLauncher = launcherComponent;
466        mService.scheduleSaveUser(mUserId);
467    }
468
469    public ComponentName getCachedLauncher() {
470        return mCachedLauncher;
471    }
472
473    public void resetThrottling() {
474        for (int i = mPackages.size() - 1; i >= 0; i--) {
475            mPackages.valueAt(i).resetThrottling();
476        }
477    }
478
479    public void mergeRestoredFile(ShortcutUser restored) {
480        final ShortcutService s = mService;
481        // Note, a restore happens only at the end of setup wizard.  At this point, no apps are
482        // installed from Play Store yet, but it's still possible that system apps have already
483        // published dynamic shortcuts, since some apps do so on BOOT_COMPLETED.
484        // When such a system app has allowbackup=true, then we go ahead and replace all existing
485        // shortcuts with the restored shortcuts.  (Then we'll re-publish manifest shortcuts later
486        // in the call site.)
487        // When such a system app has allowbackup=false, then we'll keep the shortcuts that have
488        // already been published.  So we selectively add restored ShortcutPackages here.
489        //
490        // The same logic applies to launchers, but since launchers shouldn't pin shortcuts
491        // without users interaction it's really not a big deal, so we just clear existing
492        // ShortcutLauncher instances in mLaunchers and add all the restored ones here.
493
494        int[] restoredLaunchers = new int[1];
495        int[] restoredPackages = new int[1];
496        int[] restoredShortcuts = new int[1];
497
498        mLaunchers.clear();
499        restored.forAllLaunchers(sl -> {
500            // If the app is already installed and allowbackup = false, then ignore the restored
501            // data.
502            if (s.isPackageInstalled(sl.getPackageName(), getUserId())
503                    && !s.shouldBackupApp(sl.getPackageName(), getUserId())) {
504                return;
505            }
506            addLauncher(sl);
507            restoredLaunchers[0]++;
508        });
509        restored.forAllPackages(sp -> {
510            // If the app is already installed and allowbackup = false, then ignore the restored
511            // data.
512            if (s.isPackageInstalled(sp.getPackageName(), getUserId())
513                    && !s.shouldBackupApp(sp.getPackageName(), getUserId())) {
514                return;
515            }
516
517            final ShortcutPackage previous = getPackageShortcutsIfExists(sp.getPackageName());
518            if (previous != null && previous.hasNonManifestShortcuts()) {
519                Log.w(TAG, "Shortcuts for package " + sp.getPackageName() + " are being restored."
520                        + " Existing non-manifeset shortcuts will be overwritten.");
521            }
522            addPackage(sp);
523            restoredPackages[0]++;
524            restoredShortcuts[0] += sp.getShortcutCount();
525        });
526        // Empty the launchers and packages in restored to avoid accidentally using them.
527        restored.mLaunchers.clear();
528        restored.mPackages.clear();
529
530        Slog.i(TAG, "Restored: L=" + restoredLaunchers[0]
531                + " P=" + restoredPackages[0]
532                + " S=" + restoredShortcuts[0]);
533    }
534
535    public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
536        if (filter.shouldDumpDetails()) {
537            pw.print(prefix);
538            pw.print("User: ");
539            pw.print(mUserId);
540            pw.print("  Known locales: ");
541            pw.print(mKnownLocales);
542            pw.print("  Last app scan: [");
543            pw.print(mLastAppScanTime);
544            pw.print("] ");
545            pw.print(ShortcutService.formatTime(mLastAppScanTime));
546            pw.print("  Last app scan FP: ");
547            pw.print(mLastAppScanOsFingerprint);
548            pw.println();
549
550            prefix += prefix + "  ";
551
552            pw.print(prefix);
553            pw.print("Cached launcher: ");
554            pw.print(mCachedLauncher);
555            pw.println();
556
557            pw.print(prefix);
558            pw.print("Last known launcher: ");
559            pw.print(mLastKnownLauncher);
560            pw.println();
561        }
562
563        for (int i = 0; i < mLaunchers.size(); i++) {
564            ShortcutLauncher launcher = mLaunchers.valueAt(i);
565            if (filter.isPackageMatch(launcher.getPackageName())) {
566                launcher.dump(pw, prefix, filter);
567            }
568        }
569
570        for (int i = 0; i < mPackages.size(); i++) {
571            ShortcutPackage pkg = mPackages.valueAt(i);
572            if (filter.isPackageMatch(pkg.getPackageName())) {
573                pkg.dump(pw, prefix, filter);
574            }
575        }
576
577        if (filter.shouldDumpDetails()) {
578            pw.println();
579            pw.print(prefix);
580            pw.println("Bitmap directories: ");
581            dumpDirectorySize(pw, prefix + "  ", mService.getUserBitmapFilePath(mUserId));
582        }
583    }
584
585    private void dumpDirectorySize(@NonNull PrintWriter pw,
586            @NonNull String prefix, File path) {
587        int numFiles = 0;
588        long size = 0;
589        final File[] children = path.listFiles();
590        if (children != null) {
591            for (File child : path.listFiles()) {
592                if (child.isFile()) {
593                    numFiles++;
594                    size += child.length();
595                } else if (child.isDirectory()) {
596                    dumpDirectorySize(pw, prefix + "  ", child);
597                }
598            }
599        }
600        pw.print(prefix);
601        pw.print("Path: ");
602        pw.print(path.getName());
603        pw.print("/ has ");
604        pw.print(numFiles);
605        pw.print(" files, size=");
606        pw.print(size);
607        pw.print(" (");
608        pw.print(Formatter.formatFileSize(mService.mContext, size));
609        pw.println(")");
610    }
611
612    public JSONObject dumpCheckin(boolean clear) throws JSONException {
613        final JSONObject result = new JSONObject();
614
615        result.put(KEY_USER_ID, mUserId);
616
617        {
618            final JSONArray launchers = new JSONArray();
619            for (int i = 0; i < mLaunchers.size(); i++) {
620                launchers.put(mLaunchers.valueAt(i).dumpCheckin(clear));
621            }
622            result.put(KEY_LAUNCHERS, launchers);
623        }
624
625        {
626            final JSONArray packages = new JSONArray();
627            for (int i = 0; i < mPackages.size(); i++) {
628                packages.put(mPackages.valueAt(i).dumpCheckin(clear));
629            }
630            result.put(KEY_PACKAGES, packages);
631        }
632
633        return result;
634    }
635}
636