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