ShortcutUser.java revision a4f89b1251235a7373996d0dda0d888673d8e941
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 (spi.getPackageUserId() != spi.getOwnerUserId()) {
368                return; // Don't save cross-user information.
369            }
370        }
371        spi.saveToXml(out, forBackup);
372    }
373
374    public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId,
375            boolean fromBackup) throws IOException, XmlPullParserException, InvalidFileFormatException {
376        final ShortcutUser ret = new ShortcutUser(s, userId);
377
378        try {
379            ret.mKnownLocales = ShortcutService.parseStringAttribute(parser,
380                    ATTR_KNOWN_LOCALES);
381
382            // If lastAppScanTime is in the future, that means the clock went backwards.
383            // Just scan all apps again.
384            final long lastAppScanTime = ShortcutService.parseLongAttribute(parser,
385                    ATTR_LAST_APP_SCAN_TIME);
386            final long currentTime = s.injectCurrentTimeMillis();
387            ret.mLastAppScanTime = lastAppScanTime < currentTime ? lastAppScanTime : 0;
388            ret.mLastAppScanOsFingerprint = ShortcutService.parseStringAttribute(parser,
389                    ATTR_LAST_APP_SCAN_OS_FINGERPRINT);
390            final int outerDepth = parser.getDepth();
391            int type;
392            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
393                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
394                if (type != XmlPullParser.START_TAG) {
395                    continue;
396                }
397                final int depth = parser.getDepth();
398                final String tag = parser.getName();
399
400                if (depth == outerDepth + 1) {
401                    switch (tag) {
402                        case TAG_LAUNCHER: {
403                            ret.mLastKnownLauncher = ShortcutService.parseComponentNameAttribute(
404                                    parser, ATTR_VALUE);
405                            continue;
406                        }
407                        case ShortcutPackage.TAG_ROOT: {
408                            final ShortcutPackage shortcuts = ShortcutPackage.loadFromXml(
409                                    s, ret, parser, fromBackup);
410
411                            // Don't use addShortcut(), we don't need to save the icon.
412                            ret.mPackages.put(shortcuts.getPackageName(), shortcuts);
413                            continue;
414                        }
415
416                        case ShortcutLauncher.TAG_ROOT: {
417                            ret.addLauncher(
418                                    ShortcutLauncher.loadFromXml(parser, ret, userId, fromBackup));
419                            continue;
420                        }
421                    }
422                }
423                ShortcutService.warnForInvalidTag(depth, tag);
424            }
425        } catch (RuntimeException e) {
426            throw new ShortcutService.InvalidFileFormatException(
427                    "Unable to parse file", e);
428        }
429        return ret;
430    }
431
432    public ComponentName getLastKnownLauncher() {
433        return mLastKnownLauncher;
434    }
435
436    public void setLauncher(ComponentName launcherComponent) {
437        setLauncher(launcherComponent, /* allowPurgeLastKnown */ false);
438    }
439
440    /** Clears the launcher information without clearing the last known one */
441    public void clearLauncher() {
442        setLauncher(null);
443    }
444
445    /**
446     * Clears the launcher information *with(* clearing the last known one; we do this witl
447     * "cmd shortcut clear-default-launcher".
448     */
449    public void forceClearLauncher() {
450        setLauncher(null, /* allowPurgeLastKnown */ true);
451    }
452
453    private void setLauncher(ComponentName launcherComponent, boolean allowPurgeLastKnown) {
454        mCachedLauncher = launcherComponent; // Always update the in-memory cache.
455
456        if (Objects.equal(mLastKnownLauncher, launcherComponent)) {
457            return;
458        }
459        if (!allowPurgeLastKnown && launcherComponent == null) {
460            return;
461        }
462        mLastKnownLauncher = launcherComponent;
463        mService.scheduleSaveUser(mUserId);
464    }
465
466    public ComponentName getCachedLauncher() {
467        return mCachedLauncher;
468    }
469
470    public void resetThrottling() {
471        for (int i = mPackages.size() - 1; i >= 0; i--) {
472            mPackages.valueAt(i).resetThrottling();
473        }
474    }
475
476    public void mergeRestoredFile(ShortcutUser restored) {
477        final ShortcutService s = mService;
478        // Note, a restore happens only at the end of setup wizard.  At this point, no apps are
479        // installed from Play Store yet, but it's still possible that system apps have already
480        // published dynamic shortcuts, since some apps do so on BOOT_COMPLETED.
481        // When such a system app has allowbackup=true, then we go ahead and replace all existing
482        // shortcuts with the restored shortcuts.  (Then we'll re-publish manifest shortcuts later
483        // in the call site.)
484        // When such a system app has allowbackup=false, then we'll keep the shortcuts that have
485        // already been published.  So we selectively add restored ShortcutPackages here.
486        //
487        // The same logic applies to launchers, but since launchers shouldn't pin shortcuts
488        // without users interaction it's really not a big deal, so we just clear existing
489        // ShortcutLauncher instances in mLaunchers and add all the restored ones here.
490
491        int[] restoredLaunchers = new int[1];
492        int[] restoredPackages = new int[1];
493        int[] restoredShortcuts = new int[1];
494
495        mLaunchers.clear();
496        restored.forAllLaunchers(sl -> {
497            // If the app is already installed and allowbackup = false, then ignore the restored
498            // data.
499            if (s.isPackageInstalled(sl.getPackageName(), getUserId())
500                    && !s.shouldBackupApp(sl.getPackageName(), getUserId())) {
501                return;
502            }
503            addLauncher(sl);
504            restoredLaunchers[0]++;
505        });
506        restored.forAllPackages(sp -> {
507            // If the app is already installed and allowbackup = false, then ignore the restored
508            // data.
509            if (s.isPackageInstalled(sp.getPackageName(), getUserId())
510                    && !s.shouldBackupApp(sp.getPackageName(), getUserId())) {
511                return;
512            }
513
514            final ShortcutPackage previous = getPackageShortcutsIfExists(sp.getPackageName());
515            if (previous != null && previous.hasNonManifestShortcuts()) {
516                Log.w(TAG, "Shortcuts for package " + sp.getPackageName() + " are being restored."
517                        + " Existing non-manifeset shortcuts will be overwritten.");
518            }
519            addPackage(sp);
520            restoredPackages[0]++;
521            restoredShortcuts[0] += sp.getShortcutCount();
522        });
523        // Empty the launchers and packages in restored to avoid accidentally using them.
524        restored.mLaunchers.clear();
525        restored.mPackages.clear();
526
527        Slog.i(TAG, "Restored: L=" + restoredLaunchers[0]
528                + " P=" + restoredPackages[0]
529                + " S=" + restoredShortcuts[0]);
530    }
531
532    public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
533        if (filter.shouldDumpDetails()) {
534            pw.print(prefix);
535            pw.print("User: ");
536            pw.print(mUserId);
537            pw.print("  Known locales: ");
538            pw.print(mKnownLocales);
539            pw.print("  Last app scan: [");
540            pw.print(mLastAppScanTime);
541            pw.print("] ");
542            pw.print(ShortcutService.formatTime(mLastAppScanTime));
543            pw.print("  Last app scan FP: ");
544            pw.print(mLastAppScanOsFingerprint);
545            pw.println();
546
547            prefix += prefix + "  ";
548
549            pw.print(prefix);
550            pw.print("Cached launcher: ");
551            pw.print(mCachedLauncher);
552            pw.println();
553
554            pw.print(prefix);
555            pw.print("Last known launcher: ");
556            pw.print(mLastKnownLauncher);
557            pw.println();
558        }
559
560        for (int i = 0; i < mLaunchers.size(); i++) {
561            ShortcutLauncher launcher = mLaunchers.valueAt(i);
562            if (filter.isPackageMatch(launcher.getPackageName())) {
563                launcher.dump(pw, prefix, filter);
564            }
565        }
566
567        for (int i = 0; i < mPackages.size(); i++) {
568            ShortcutPackage pkg = mPackages.valueAt(i);
569            if (filter.isPackageMatch(pkg.getPackageName())) {
570                pkg.dump(pw, prefix, filter);
571            }
572        }
573
574        if (filter.shouldDumpDetails()) {
575            pw.println();
576            pw.print(prefix);
577            pw.println("Bitmap directories: ");
578            dumpDirectorySize(pw, prefix + "  ", mService.getUserBitmapFilePath(mUserId));
579        }
580    }
581
582    private void dumpDirectorySize(@NonNull PrintWriter pw,
583            @NonNull String prefix, File path) {
584        int numFiles = 0;
585        long size = 0;
586        final File[] children = path.listFiles();
587        if (children != null) {
588            for (File child : path.listFiles()) {
589                if (child.isFile()) {
590                    numFiles++;
591                    size += child.length();
592                } else if (child.isDirectory()) {
593                    dumpDirectorySize(pw, prefix + "  ", child);
594                }
595            }
596        }
597        pw.print(prefix);
598        pw.print("Path: ");
599        pw.print(path.getName());
600        pw.print("/ has ");
601        pw.print(numFiles);
602        pw.print(" files, size=");
603        pw.print(size);
604        pw.print(" (");
605        pw.print(Formatter.formatFileSize(mService.mContext, size));
606        pw.println(")");
607    }
608
609    public JSONObject dumpCheckin(boolean clear) throws JSONException {
610        final JSONObject result = new JSONObject();
611
612        result.put(KEY_USER_ID, mUserId);
613
614        {
615            final JSONArray launchers = new JSONArray();
616            for (int i = 0; i < mLaunchers.size(); i++) {
617                launchers.put(mLaunchers.valueAt(i).dumpCheckin(clear));
618            }
619            result.put(KEY_LAUNCHERS, launchers);
620        }
621
622        {
623            final JSONArray packages = new JSONArray();
624            for (int i = 0; i < mPackages.size(); i++) {
625                packages.put(mPackages.valueAt(i).dumpCheckin(clear));
626            }
627            result.put(KEY_PACKAGES, packages);
628        }
629
630        return result;
631    }
632}
633