/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.pm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.pm.ShortcutManager; import android.text.TextUtils; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.server.pm.ShortcutService.InvalidFileFormatException; import libcore.util.Objects; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.function.Consumer; /** * User information used by {@link ShortcutService}. * * All methods should be guarded by {@code #mService.mLock}. */ class ShortcutUser { private static final String TAG = ShortcutService.TAG; static final String TAG_ROOT = "user"; private static final String TAG_LAUNCHER = "launcher"; private static final String ATTR_VALUE = "value"; private static final String ATTR_KNOWN_LOCALES = "locales"; // Suffix "2" was added to force rescan all packages after the next OTA. private static final String ATTR_LAST_APP_SCAN_TIME = "last-app-scan-time2"; private static final String ATTR_LAST_APP_SCAN_OS_FINGERPRINT = "last-app-scan-fp"; private static final String KEY_USER_ID = "userId"; private static final String KEY_LAUNCHERS = "launchers"; private static final String KEY_PACKAGES = "packages"; static final class PackageWithUser { final int userId; final String packageName; private PackageWithUser(int userId, String packageName) { this.userId = userId; this.packageName = Preconditions.checkNotNull(packageName); } public static PackageWithUser of(int userId, String packageName) { return new PackageWithUser(userId, packageName); } public static PackageWithUser of(ShortcutPackageItem spi) { return new PackageWithUser(spi.getPackageUserId(), spi.getPackageName()); } @Override public int hashCode() { return packageName.hashCode() ^ userId; } @Override public boolean equals(Object obj) { if (!(obj instanceof PackageWithUser)) { return false; } final PackageWithUser that = (PackageWithUser) obj; return userId == that.userId && packageName.equals(that.packageName); } @Override public String toString() { return String.format("[Package: %d, %s]", userId, packageName); } } final ShortcutService mService; @UserIdInt private final int mUserId; private final ArrayMap mPackages = new ArrayMap<>(); private final ArrayMap mLaunchers = new ArrayMap<>(); /** * Last known launcher. It's used when the default launcher isn't set in PM -- i.e. * when getHomeActivitiesAsUser() return null. We need it so that in this situation the * previously default launcher can still access shortcuts. */ private ComponentName mLastKnownLauncher; /** In-memory-cached default launcher. */ private ComponentName mCachedLauncher; private String mKnownLocales; private long mLastAppScanTime; private String mLastAppScanOsFingerprint; public ShortcutUser(ShortcutService service, int userId) { mService = service; mUserId = userId; } public int getUserId() { return mUserId; } public long getLastAppScanTime() { return mLastAppScanTime; } public void setLastAppScanTime(long lastAppScanTime) { mLastAppScanTime = lastAppScanTime; } public String getLastAppScanOsFingerprint() { return mLastAppScanOsFingerprint; } public void setLastAppScanOsFingerprint(String lastAppScanOsFingerprint) { mLastAppScanOsFingerprint = lastAppScanOsFingerprint; } // We don't expose this directly to non-test code because only ShortcutUser should add to/ // remove from it. @VisibleForTesting ArrayMap getAllPackagesForTest() { return mPackages; } public boolean hasPackage(@NonNull String packageName) { return mPackages.containsKey(packageName); } private void addPackage(@NonNull ShortcutPackage p) { p.replaceUser(this); mPackages.put(p.getPackageName(), p); } public ShortcutPackage removePackage(@NonNull String packageName) { final ShortcutPackage removed = mPackages.remove(packageName); mService.cleanupBitmapsForPackage(mUserId, packageName); return removed; } // We don't expose this directly to non-test code because only ShortcutUser should add to/ // remove from it. @VisibleForTesting ArrayMap getAllLaunchersForTest() { return mLaunchers; } private void addLauncher(ShortcutLauncher launcher) { launcher.replaceUser(this); mLaunchers.put(PackageWithUser.of(launcher.getPackageUserId(), launcher.getPackageName()), launcher); } @Nullable public ShortcutLauncher removeLauncher( @UserIdInt int packageUserId, @NonNull String packageName) { return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName)); } @Nullable public ShortcutPackage getPackageShortcutsIfExists(@NonNull String packageName) { final ShortcutPackage ret = mPackages.get(packageName); if (ret != null) { ret.attemptToRestoreIfNeededAndSave(); } return ret; } @NonNull public ShortcutPackage getPackageShortcuts(@NonNull String packageName) { ShortcutPackage ret = getPackageShortcutsIfExists(packageName); if (ret == null) { ret = new ShortcutPackage(this, mUserId, packageName); mPackages.put(packageName, ret); } return ret; } @NonNull public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName, @UserIdInt int launcherUserId) { final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName); ShortcutLauncher ret = mLaunchers.get(key); if (ret == null) { ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId); mLaunchers.put(key, ret); } else { ret.attemptToRestoreIfNeededAndSave(); } return ret; } public void forAllPackages(Consumer callback) { final int size = mPackages.size(); for (int i = 0; i < size; i++) { callback.accept(mPackages.valueAt(i)); } } public void forAllLaunchers(Consumer callback) { final int size = mLaunchers.size(); for (int i = 0; i < size; i++) { callback.accept(mLaunchers.valueAt(i)); } } public void forAllPackageItems(Consumer callback) { forAllLaunchers(callback); forAllPackages(callback); } public void forPackageItem(@NonNull String packageName, @UserIdInt int packageUserId, Consumer callback) { forAllPackageItems(spi -> { if ((spi.getPackageUserId() == packageUserId) && spi.getPackageName().equals(packageName)) { callback.accept(spi); } }); } /** * Must be called at any entry points on {@link ShortcutManager} APIs to make sure the * information on the package is up-to-date. * * We use broadcasts to handle locale changes and package changes, but because broadcasts * are asynchronous, there's a chance a publisher calls getXxxShortcuts() after a certain event * (e.g. system locale change) but shortcut manager hasn't finished processing the broadcast. * * So we call this method at all entry points from publishers to make sure we update all * relevant information. * * Similar inconsistencies can happen when the launcher fetches shortcut information, but * that's a less of an issue because for the launcher we report shortcut changes with * callbacks. */ public void onCalledByPublisher(@NonNull String packageName) { detectLocaleChange(); rescanPackageIfNeeded(packageName, /*forceRescan=*/ false); } private String getKnownLocales() { if (TextUtils.isEmpty(mKnownLocales)) { mKnownLocales = mService.injectGetLocaleTagsForUser(mUserId); mService.scheduleSaveUser(mUserId); } return mKnownLocales; } /** * Check to see if the system locale has changed, and if so, reset throttling * and update resource strings. */ public void detectLocaleChange() { final String currentLocales = mService.injectGetLocaleTagsForUser(mUserId); if (getKnownLocales().equals(currentLocales)) { return; } if (ShortcutService.DEBUG) { Slog.d(TAG, "Locale changed from " + currentLocales + " to " + mKnownLocales + " for user " + mUserId); } mKnownLocales = currentLocales; forAllPackages(pkg -> { pkg.resetRateLimiting(); pkg.resolveResourceStrings(); }); mService.scheduleSaveUser(mUserId); } public void rescanPackageIfNeeded(@NonNull String packageName, boolean forceRescan) { final boolean isNewApp = !mPackages.containsKey(packageName); final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName); if (!shortcutPackage.rescanPackageIfNeeded(isNewApp, forceRescan)) { if (isNewApp) { mPackages.remove(packageName); } } } public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName, @UserIdInt int packageUserId) { forPackageItem(packageName, packageUserId, spi -> { spi.attemptToRestoreIfNeededAndSave(); }); } public void saveToXml(XmlSerializer out, boolean forBackup) throws IOException, XmlPullParserException { out.startTag(null, TAG_ROOT); if (!forBackup) { // Don't have to back them up. ShortcutService.writeAttr(out, ATTR_KNOWN_LOCALES, mKnownLocales); ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_TIME, mLastAppScanTime); ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_OS_FINGERPRINT, mLastAppScanOsFingerprint); ShortcutService.writeTagValue(out, TAG_LAUNCHER, mLastKnownLauncher); } // Can't use forEachPackageItem due to the checked exceptions. { final int size = mLaunchers.size(); for (int i = 0; i < size; i++) { saveShortcutPackageItem(out, mLaunchers.valueAt(i), forBackup); } } { final int size = mPackages.size(); for (int i = 0; i < size; i++) { saveShortcutPackageItem(out, mPackages.valueAt(i), forBackup); } } out.endTag(null, TAG_ROOT); } private void saveShortcutPackageItem(XmlSerializer out, ShortcutPackageItem spi, boolean forBackup) throws IOException, XmlPullParserException { if (forBackup) { if (!mService.shouldBackupApp(spi.getPackageName(), spi.getPackageUserId())) { return; // Don't save. } if (spi.getPackageUserId() != spi.getOwnerUserId()) { return; // Don't save cross-user information. } } spi.saveToXml(out, forBackup); } public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId, boolean fromBackup) throws IOException, XmlPullParserException, InvalidFileFormatException { final ShortcutUser ret = new ShortcutUser(s, userId); try { ret.mKnownLocales = ShortcutService.parseStringAttribute(parser, ATTR_KNOWN_LOCALES); // If lastAppScanTime is in the future, that means the clock went backwards. // Just scan all apps again. final long lastAppScanTime = ShortcutService.parseLongAttribute(parser, ATTR_LAST_APP_SCAN_TIME); final long currentTime = s.injectCurrentTimeMillis(); ret.mLastAppScanTime = lastAppScanTime < currentTime ? lastAppScanTime : 0; ret.mLastAppScanOsFingerprint = ShortcutService.parseStringAttribute(parser, ATTR_LAST_APP_SCAN_OS_FINGERPRINT); final int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type != XmlPullParser.START_TAG) { continue; } final int depth = parser.getDepth(); final String tag = parser.getName(); if (depth == outerDepth + 1) { switch (tag) { case TAG_LAUNCHER: { ret.mLastKnownLauncher = ShortcutService.parseComponentNameAttribute( parser, ATTR_VALUE); continue; } case ShortcutPackage.TAG_ROOT: { final ShortcutPackage shortcuts = ShortcutPackage.loadFromXml( s, ret, parser, fromBackup); // Don't use addShortcut(), we don't need to save the icon. ret.mPackages.put(shortcuts.getPackageName(), shortcuts); continue; } case ShortcutLauncher.TAG_ROOT: { ret.addLauncher( ShortcutLauncher.loadFromXml(parser, ret, userId, fromBackup)); continue; } } } ShortcutService.warnForInvalidTag(depth, tag); } } catch (RuntimeException e) { throw new ShortcutService.InvalidFileFormatException( "Unable to parse file", e); } return ret; } public ComponentName getLastKnownLauncher() { return mLastKnownLauncher; } public void setLauncher(ComponentName launcherComponent) { setLauncher(launcherComponent, /* allowPurgeLastKnown */ false); } /** Clears the launcher information without clearing the last known one */ public void clearLauncher() { setLauncher(null); } /** * Clears the launcher information *with(* clearing the last known one; we do this witl * "cmd shortcut clear-default-launcher". */ public void forceClearLauncher() { setLauncher(null, /* allowPurgeLastKnown */ true); } private void setLauncher(ComponentName launcherComponent, boolean allowPurgeLastKnown) { mCachedLauncher = launcherComponent; // Always update the in-memory cache. if (Objects.equal(mLastKnownLauncher, launcherComponent)) { return; } if (!allowPurgeLastKnown && launcherComponent == null) { return; } mLastKnownLauncher = launcherComponent; mService.scheduleSaveUser(mUserId); } public ComponentName getCachedLauncher() { return mCachedLauncher; } public void resetThrottling() { for (int i = mPackages.size() - 1; i >= 0; i--) { mPackages.valueAt(i).resetThrottling(); } } public void mergeRestoredFile(ShortcutUser restored) { final ShortcutService s = mService; // Note, a restore happens only at the end of setup wizard. At this point, no apps are // installed from Play Store yet, but it's still possible that system apps have already // published dynamic shortcuts, since some apps do so on BOOT_COMPLETED. // When such a system app has allowbackup=true, then we go ahead and replace all existing // shortcuts with the restored shortcuts. (Then we'll re-publish manifest shortcuts later // in the call site.) // When such a system app has allowbackup=false, then we'll keep the shortcuts that have // already been published. So we selectively add restored ShortcutPackages here. // // The same logic applies to launchers, but since launchers shouldn't pin shortcuts // without users interaction it's really not a big deal, so we just clear existing // ShortcutLauncher instances in mLaunchers and add all the restored ones here. int[] restoredLaunchers = new int[1]; int[] restoredPackages = new int[1]; int[] restoredShortcuts = new int[1]; mLaunchers.clear(); restored.forAllLaunchers(sl -> { // If the app is already installed and allowbackup = false, then ignore the restored // data. if (s.isPackageInstalled(sl.getPackageName(), getUserId()) && !s.shouldBackupApp(sl.getPackageName(), getUserId())) { return; } addLauncher(sl); restoredLaunchers[0]++; }); restored.forAllPackages(sp -> { // If the app is already installed and allowbackup = false, then ignore the restored // data. if (s.isPackageInstalled(sp.getPackageName(), getUserId()) && !s.shouldBackupApp(sp.getPackageName(), getUserId())) { return; } final ShortcutPackage previous = getPackageShortcutsIfExists(sp.getPackageName()); if (previous != null && previous.hasNonManifestShortcuts()) { Log.w(TAG, "Shortcuts for package " + sp.getPackageName() + " are being restored." + " Existing non-manifeset shortcuts will be overwritten."); } addPackage(sp); restoredPackages[0]++; restoredShortcuts[0] += sp.getShortcutCount(); }); // Empty the launchers and packages in restored to avoid accidentally using them. restored.mLaunchers.clear(); restored.mPackages.clear(); Slog.i(TAG, "Restored: L=" + restoredLaunchers[0] + " P=" + restoredPackages[0] + " S=" + restoredShortcuts[0]); } public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.print(prefix); pw.print("User: "); pw.print(mUserId); pw.print(" Known locales: "); pw.print(mKnownLocales); pw.print(" Last app scan: ["); pw.print(mLastAppScanTime); pw.print("] "); pw.print(ShortcutService.formatTime(mLastAppScanTime)); pw.print(" Last app scan FP: "); pw.print(mLastAppScanOsFingerprint); pw.println(); prefix += prefix + " "; pw.print(prefix); pw.print("Cached launcher: "); pw.print(mCachedLauncher); pw.println(); pw.print(prefix); pw.print("Last known launcher: "); pw.print(mLastKnownLauncher); pw.println(); for (int i = 0; i < mLaunchers.size(); i++) { mLaunchers.valueAt(i).dump(pw, prefix); } for (int i = 0; i < mPackages.size(); i++) { mPackages.valueAt(i).dump(pw, prefix); } pw.println(); pw.print(prefix); pw.println("Bitmap directories: "); dumpDirectorySize(pw, prefix + " ", mService.getUserBitmapFilePath(mUserId)); } private void dumpDirectorySize(@NonNull PrintWriter pw, @NonNull String prefix, File path) { int numFiles = 0; long size = 0; final File[] children = path.listFiles(); if (children != null) { for (File child : path.listFiles()) { if (child.isFile()) { numFiles++; size += child.length(); } else if (child.isDirectory()) { dumpDirectorySize(pw, prefix + " ", child); } } } pw.print(prefix); pw.print("Path: "); pw.print(path.getName()); pw.print("/ has "); pw.print(numFiles); pw.print(" files, size="); pw.print(size); pw.print(" ("); pw.print(Formatter.formatFileSize(mService.mContext, size)); pw.println(")"); } public JSONObject dumpCheckin(boolean clear) throws JSONException { final JSONObject result = new JSONObject(); result.put(KEY_USER_ID, mUserId); { final JSONArray launchers = new JSONArray(); for (int i = 0; i < mLaunchers.size(); i++) { launchers.put(mLaunchers.valueAt(i).dumpCheckin(clear)); } result.put(KEY_LAUNCHERS, launchers); } { final JSONArray packages = new JSONArray(); for (int i = 0; i < mPackages.size(); i++) { packages.put(mPackages.valueAt(i).dumpCheckin(clear)); } result.put(KEY_PACKAGES, packages); } return result; } }