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