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