ShortcutUser.java revision 76269928e677725e2d9b28e2e3aa79961a60a1d0
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.Slog;
27import android.util.SparseArray;
28
29import com.android.internal.annotations.GuardedBy;
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.internal.util.Preconditions;
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    private static final String ATTR_LAST_APP_SCAN_TIME = "last-app-scan-time";
61    private static final String KEY_USER_ID = "userId";
62    private static final String KEY_LAUNCHERS = "launchers";
63    private static final String KEY_PACKAGES = "packages";
64
65    static final class PackageWithUser {
66        final int userId;
67        final String packageName;
68
69        private PackageWithUser(int userId, String packageName) {
70            this.userId = userId;
71            this.packageName = Preconditions.checkNotNull(packageName);
72        }
73
74        public static PackageWithUser of(int userId, String packageName) {
75            return new PackageWithUser(userId, packageName);
76        }
77
78        public static PackageWithUser of(ShortcutPackageItem spi) {
79            return new PackageWithUser(spi.getPackageUserId(), spi.getPackageName());
80        }
81
82        @Override
83        public int hashCode() {
84            return packageName.hashCode() ^ userId;
85        }
86
87        @Override
88        public boolean equals(Object obj) {
89            if (!(obj instanceof PackageWithUser)) {
90                return false;
91            }
92            final PackageWithUser that = (PackageWithUser) obj;
93
94            return userId == that.userId && packageName.equals(that.packageName);
95        }
96
97        @Override
98        public String toString() {
99            return String.format("[Package: %d, %s]", userId, packageName);
100        }
101    }
102
103    final ShortcutService mService;
104
105    @UserIdInt
106    private final int mUserId;
107
108    private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
109
110    private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
111
112    /**
113     * Last known launcher.  It's used when the default launcher isn't set in PM -- i.e.
114     * when getHomeActivitiesAsUser() return null.  We need it so that in this situation the
115     * previously default launcher can still access shortcuts.
116     */
117    private ComponentName mLastKnownLauncher;
118
119    /** In-memory-cached default launcher. */
120    private ComponentName mCachedLauncher;
121
122    private String mKnownLocales;
123
124    private long mLastAppScanTime;
125
126    public ShortcutUser(ShortcutService service, int userId) {
127        mService = service;
128        mUserId = userId;
129    }
130
131    public int getUserId() {
132        return mUserId;
133    }
134
135    public long getLastAppScanTime() {
136        return mLastAppScanTime;
137    }
138
139    public void setLastAppScanTime(long lastAppScanTime) {
140        mLastAppScanTime = lastAppScanTime;
141    }
142
143    // We don't expose this directly to non-test code because only ShortcutUser should add to/
144    // remove from it.
145    @VisibleForTesting
146    ArrayMap<String, ShortcutPackage> getAllPackagesForTest() {
147        return mPackages;
148    }
149
150    public boolean hasPackage(@NonNull String packageName) {
151        return mPackages.containsKey(packageName);
152    }
153
154    public ShortcutPackage removePackage(@NonNull String packageName) {
155        final ShortcutPackage removed = mPackages.remove(packageName);
156
157        mService.cleanupBitmapsForPackage(mUserId, packageName);
158
159        return removed;
160    }
161
162    // We don't expose this directly to non-test code because only ShortcutUser should add to/
163    // remove from it.
164    @VisibleForTesting
165    ArrayMap<PackageWithUser, ShortcutLauncher> getAllLaunchersForTest() {
166        return mLaunchers;
167    }
168
169    public void addLauncher(ShortcutLauncher launcher) {
170        mLaunchers.put(PackageWithUser.of(launcher.getPackageUserId(),
171                launcher.getPackageName()), launcher);
172    }
173
174    @Nullable
175    public ShortcutLauncher removeLauncher(
176            @UserIdInt int packageUserId, @NonNull String packageName) {
177        return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName));
178    }
179
180    @Nullable
181    public ShortcutPackage getPackageShortcutsIfExists(@NonNull String packageName) {
182        final ShortcutPackage ret = mPackages.get(packageName);
183        if (ret != null) {
184            ret.attemptToRestoreIfNeededAndSave();
185        }
186        return ret;
187    }
188
189    @NonNull
190    public ShortcutPackage getPackageShortcuts(@NonNull String packageName) {
191        ShortcutPackage ret = getPackageShortcutsIfExists(packageName);
192        if (ret == null) {
193            ret = new ShortcutPackage(this, mUserId, packageName);
194            mPackages.put(packageName, ret);
195        }
196        return ret;
197    }
198
199    @NonNull
200    public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName,
201            @UserIdInt int launcherUserId) {
202        final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName);
203        ShortcutLauncher ret = mLaunchers.get(key);
204        if (ret == null) {
205            ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
206            mLaunchers.put(key, ret);
207        } else {
208            ret.attemptToRestoreIfNeededAndSave();
209        }
210        return ret;
211    }
212
213    public void forAllPackages(Consumer<? super ShortcutPackage> callback) {
214        final int size = mPackages.size();
215        for (int i = 0; i < size; i++) {
216            callback.accept(mPackages.valueAt(i));
217        }
218    }
219
220    public void forAllLaunchers(Consumer<? super ShortcutLauncher> callback) {
221        final int size = mLaunchers.size();
222        for (int i = 0; i < size; i++) {
223            callback.accept(mLaunchers.valueAt(i));
224        }
225    }
226
227    public void forAllPackageItems(Consumer<? super ShortcutPackageItem> callback) {
228        forAllLaunchers(callback);
229        forAllPackages(callback);
230    }
231
232    public void forPackageItem(@NonNull String packageName, @UserIdInt int packageUserId,
233            Consumer<ShortcutPackageItem> callback) {
234        forAllPackageItems(spi -> {
235            if ((spi.getPackageUserId() == packageUserId)
236                    && spi.getPackageName().equals(packageName)) {
237                callback.accept(spi);
238            }
239        });
240    }
241
242    /**
243     * Must be called at any entry points on {@link ShortcutManager} APIs to make sure the
244     * information on the package is up-to-date.
245     *
246     * We use broadcasts to handle locale changes and package changes, but because broadcasts
247     * are asynchronous, there's a chance a publisher calls getXxxShortcuts() after a certain event
248     * (e.g. system locale change) but shortcut manager hasn't finished processing the broadcast.
249     *
250     * So we call this method at all entry points from publishers to make sure we update all
251     * relevant information.
252     *
253     * Similar inconsistencies can happen when the launcher fetches shortcut information, but
254     * that's a less of an issue because for the launcher we report shortcut changes with
255     * callbacks.
256     */
257    public void onCalledByPublisher(@NonNull String packageName) {
258        detectLocaleChange();
259        rescanPackageIfNeeded(packageName, /*forceRescan=*/ false);
260    }
261
262    private String getKnownLocales() {
263        if (TextUtils.isEmpty(mKnownLocales)) {
264            mKnownLocales = mService.injectGetLocaleTagsForUser(mUserId);
265            mService.scheduleSaveUser(mUserId);
266        }
267        return mKnownLocales;
268    }
269
270    /**
271     * Check to see if the system locale has changed, and if so, reset throttling
272     * and update resource strings.
273     */
274    public void detectLocaleChange() {
275        final String currentLocales = mService.injectGetLocaleTagsForUser(mUserId);
276        if (getKnownLocales().equals(currentLocales)) {
277            return;
278        }
279        if (ShortcutService.DEBUG) {
280            Slog.d(TAG, "Locale changed from " + currentLocales + " to " + mKnownLocales
281                    + " for user " + mUserId);
282        }
283        mKnownLocales = currentLocales;
284
285        forAllPackages(pkg -> {
286            pkg.resetRateLimiting();
287            pkg.resolveResourceStrings();
288        });
289
290        mService.scheduleSaveUser(mUserId);
291    }
292
293    public void rescanPackageIfNeeded(@NonNull String packageName, boolean forceRescan) {
294        final boolean isNewApp = !mPackages.containsKey(packageName);
295
296        final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
297
298        if (!shortcutPackage.rescanPackageIfNeeded(isNewApp, forceRescan)) {
299            if (isNewApp) {
300                mPackages.remove(packageName);
301            }
302        }
303    }
304
305    public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName,
306            @UserIdInt int packageUserId) {
307        forPackageItem(packageName, packageUserId, spi -> {
308            spi.attemptToRestoreIfNeededAndSave();
309        });
310    }
311
312    public void saveToXml(XmlSerializer out, boolean forBackup)
313            throws IOException, XmlPullParserException {
314        out.startTag(null, TAG_ROOT);
315
316        ShortcutService.writeAttr(out, ATTR_KNOWN_LOCALES, mKnownLocales);
317        ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_TIME,
318                mLastAppScanTime);
319
320        ShortcutService.writeTagValue(out, TAG_LAUNCHER, mLastKnownLauncher);
321
322        // Can't use forEachPackageItem due to the checked exceptions.
323        {
324            final int size = mLaunchers.size();
325            for (int i = 0; i < size; i++) {
326                saveShortcutPackageItem(out, mLaunchers.valueAt(i), forBackup);
327            }
328        }
329        {
330            final int size = mPackages.size();
331            for (int i = 0; i < size; i++) {
332                saveShortcutPackageItem(out, mPackages.valueAt(i), forBackup);
333            }
334        }
335
336        out.endTag(null, TAG_ROOT);
337    }
338
339    private void saveShortcutPackageItem(XmlSerializer out,
340            ShortcutPackageItem spi, boolean forBackup) throws IOException, XmlPullParserException {
341        if (forBackup) {
342            if (!mService.shouldBackupApp(spi.getPackageName(), spi.getPackageUserId())) {
343                return; // Don't save.
344            }
345            if (spi.getPackageUserId() != spi.getOwnerUserId()) {
346                return; // Don't save cross-user information.
347            }
348        }
349        spi.saveToXml(out, forBackup);
350    }
351
352    public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId,
353            boolean fromBackup) throws IOException, XmlPullParserException {
354        final ShortcutUser ret = new ShortcutUser(s, userId);
355
356        ret.mKnownLocales = ShortcutService.parseStringAttribute(parser,
357                ATTR_KNOWN_LOCALES);
358
359        // If lastAppScanTime is in the future, that means the clock went backwards.
360        // Just scan all apps again.
361        final long lastAppScanTime = ShortcutService.parseLongAttribute(parser,
362                ATTR_LAST_APP_SCAN_TIME);
363        final long currentTime = s.injectCurrentTimeMillis();
364        ret.mLastAppScanTime = lastAppScanTime < currentTime ? lastAppScanTime : 0;
365
366        final int outerDepth = parser.getDepth();
367        int type;
368        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
369                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
370            if (type != XmlPullParser.START_TAG) {
371                continue;
372            }
373            final int depth = parser.getDepth();
374            final String tag = parser.getName();
375
376            if (depth == outerDepth + 1) {
377                switch (tag) {
378                    case TAG_LAUNCHER: {
379                        ret.mLastKnownLauncher = ShortcutService.parseComponentNameAttribute(
380                                parser, ATTR_VALUE);
381                        continue;
382                    }
383                    case ShortcutPackage.TAG_ROOT: {
384                        final ShortcutPackage shortcuts = ShortcutPackage.loadFromXml(
385                                s, ret, parser, fromBackup);
386
387                        // Don't use addShortcut(), we don't need to save the icon.
388                        ret.mPackages.put(shortcuts.getPackageName(), shortcuts);
389                        continue;
390                    }
391
392                    case ShortcutLauncher.TAG_ROOT: {
393                        ret.addLauncher(
394                                ShortcutLauncher.loadFromXml(parser, ret, userId, fromBackup));
395                        continue;
396                    }
397                }
398            }
399            ShortcutService.warnForInvalidTag(depth, tag);
400        }
401        return ret;
402    }
403
404    public ComponentName getLastKnownLauncher() {
405        return mLastKnownLauncher;
406    }
407
408    public void setLauncher(ComponentName launcherComponent) {
409        setLauncher(launcherComponent, /* allowPurgeLastKnown */ false);
410    }
411
412    /** Clears the launcher information without clearing the last known one */
413    public void clearLauncher() {
414        setLauncher(null);
415    }
416
417    /**
418     * Clears the launcher information *with(* clearing the last known one; we do this witl
419     * "cmd shortcut clear-default-launcher".
420     */
421    public void forceClearLauncher() {
422        setLauncher(null, /* allowPurgeLastKnown */ true);
423    }
424
425    private void setLauncher(ComponentName launcherComponent, boolean allowPurgeLastKnown) {
426        mCachedLauncher = launcherComponent; // Always update the in-memory cache.
427
428        if (Objects.equal(mLastKnownLauncher, launcherComponent)) {
429            return;
430        }
431        if (!allowPurgeLastKnown && launcherComponent == null) {
432            return;
433        }
434        mLastKnownLauncher = launcherComponent;
435        mService.scheduleSaveUser(mUserId);
436    }
437
438    public ComponentName getCachedLauncher() {
439        return mCachedLauncher;
440    }
441
442    public void resetThrottling() {
443        for (int i = mPackages.size() - 1; i >= 0; i--) {
444            mPackages.valueAt(i).resetThrottling();
445        }
446    }
447
448    public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
449        pw.print(prefix);
450        pw.print("User: ");
451        pw.print(mUserId);
452        pw.print("  Known locales: ");
453        pw.print(mKnownLocales);
454        pw.print("  Last app scan: [");
455        pw.print(mLastAppScanTime);
456        pw.print("] ");
457        pw.print(ShortcutService.formatTime(mLastAppScanTime));
458        pw.println();
459
460        prefix += prefix + "  ";
461
462        pw.print(prefix);
463        pw.print("Cached launcher: ");
464        pw.print(mCachedLauncher);
465        pw.println();
466
467        pw.print(prefix);
468        pw.print("Last known launcher: ");
469        pw.print(mLastKnownLauncher);
470        pw.println();
471
472        for (int i = 0; i < mLaunchers.size(); i++) {
473            mLaunchers.valueAt(i).dump(pw, prefix);
474        }
475
476        for (int i = 0; i < mPackages.size(); i++) {
477            mPackages.valueAt(i).dump(pw, prefix);
478        }
479
480        pw.println();
481        pw.print(prefix);
482        pw.println("Bitmap directories: ");
483        dumpDirectorySize(pw, prefix + "  ", mService.getUserBitmapFilePath(mUserId));
484    }
485
486    private void dumpDirectorySize(@NonNull PrintWriter pw,
487            @NonNull String prefix, File path) {
488        int numFiles = 0;
489        long size = 0;
490        final File[] children = path.listFiles();
491        if (children != null) {
492            for (File child : path.listFiles()) {
493                if (child.isFile()) {
494                    numFiles++;
495                    size += child.length();
496                } else if (child.isDirectory()) {
497                    dumpDirectorySize(pw, prefix + "  ", child);
498                }
499            }
500        }
501        pw.print(prefix);
502        pw.print("Path: ");
503        pw.print(path.getName());
504        pw.print("/ has ");
505        pw.print(numFiles);
506        pw.print(" files, size=");
507        pw.print(size);
508        pw.print(" (");
509        pw.print(Formatter.formatFileSize(mService.mContext, size));
510        pw.println(")");
511    }
512
513    public JSONObject dumpCheckin(boolean clear) throws JSONException {
514        final JSONObject result = new JSONObject();
515
516        result.put(KEY_USER_ID, mUserId);
517
518        {
519            final JSONArray launchers = new JSONArray();
520            for (int i = 0; i < mLaunchers.size(); i++) {
521                launchers.put(mLaunchers.valueAt(i).dumpCheckin(clear));
522            }
523            result.put(KEY_LAUNCHERS, launchers);
524        }
525
526        {
527            final JSONArray packages = new JSONArray();
528            for (int i = 0; i < mPackages.size(); i++) {
529                packages.put(mPackages.valueAt(i).dumpCheckin(clear));
530            }
531            result.put(KEY_PACKAGES, packages);
532        }
533
534        return result;
535    }
536}
537