content: /*
   * 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.metrics.LogMaker;
  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.logging.MetricsLogger;
  import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
  import com.android.internal.util.Preconditions;
  import com.android.server.pm.ShortcutService.DumpFilter;
  import com.android.server.pm.ShortcutService.InvalidFileFormatException;
  
  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.Objects;
  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 ATTR_RESTORE_SOURCE_FINGERPRINT = "restore-from-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<String, ShortcutPackage> mPackages = new ArrayMap<>();
  
      private final ArrayMap<PackageWithUser, ShortcutLauncher> 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;
      private String mRestoreFromOsFingerprint;
  
      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<String, ShortcutPackage> 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<PackageWithUser, ShortcutLauncher> 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<? super ShortcutPackage> callback) {
          final int size = mPackages.size();
          for (int i = 0; i < size; i++) {
              callback.accept(mPackages.valueAt(i));
          }
      }
  
      public void forAllLaunchers(Consumer<? super ShortcutLauncher> callback) {
          final int size = mLaunchers.size();
          for (int i = 0; i < size; i++) {
              callback.accept(mLaunchers.valueAt(i));
          }
      }
  
      public void forAllPackageItems(Consumer<? super ShortcutPackageItem> callback) {
          forAllLaunchers(callback);
          forAllPackages(callback);
      }
  
      public void forPackageItem(@NonNull String packageName, @UserIdInt int packageUserId,
              Consumer<ShortcutPackageItem> 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 (!TextUtils.isEmpty(mKnownLocales) && mKnownLocales.equals(currentLocales)) {
              return;
          }
          if (ShortcutService.DEBUG) {
              Slog.d(TAG, "Locale changed from " + mKnownLocales + " to " + currentLocales
                      + " 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.writeAttr(out, ATTR_RESTORE_SOURCE_FINGERPRINT,
                      mRestoreFromOsFingerprint);
  
              ShortcutService.writeTagValue(out, TAG_LAUNCHER, mLastKnownLauncher);
          } else {
              ShortcutService.writeAttr(out, ATTR_RESTORE_SOURCE_FINGERPRINT,
                      mService.injectBuildFingerprint());
          }
  
          // 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 (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);
              ret.mRestoreFromOsFingerprint = ShortcutService.parseStringAttribute(parser,
                      ATTR_RESTORE_SOURCE_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.equals(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();
  
          mRestoreFromOsFingerprint = restored.mRestoreFromOsFingerprint;
  
          Slog.i(TAG, "Restored: L=" + restoredLaunchers[0]
                  + " P=" + restoredPackages[0]
                  + " S=" + restoredShortcuts[0]);
      }
  
      public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
          if (filter.shouldDumpDetails()) {
              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.println(ShortcutService.formatTime(mLastAppScanTime));
  
              prefix += prefix + "  ";
  
              pw.print(prefix);
              pw.print("Last app scan FP: ");
              pw.println(mLastAppScanOsFingerprint);
  
              pw.print(prefix);
              pw.print("Restore from FP: ");
              pw.print(mRestoreFromOsFingerprint);
              pw.println();
  
  
              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++) {
              ShortcutLauncher launcher = mLaunchers.valueAt(i);
              if (filter.isPackageMatch(launcher.getPackageName())) {
                  launcher.dump(pw, prefix, filter);
              }
          }
  
          for (int i = 0; i < mPackages.size(); i++) {
              ShortcutPackage pkg = mPackages.valueAt(i);
              if (filter.isPackageMatch(pkg.getPackageName())) {
                  pkg.dump(pw, prefix, filter);
              }
          }
  
          if (filter.shouldDumpDetails()) {
              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;
      }
  
      void logSharingShortcutStats(MetricsLogger logger) {
          int packageWithShareTargetsCount = 0;
          int totalSharingShortcutCount = 0;
          for (int i = 0; i < mPackages.size(); i++) {
              if (mPackages.valueAt(i).hasShareTargets()) {
                  packageWithShareTargetsCount++;
                  totalSharingShortcutCount += mPackages.valueAt(i).getSharingShortcutCount();
              }
          }
  
          final LogMaker logMaker = new LogMaker(MetricsEvent.ACTION_SHORTCUTS_CHANGED);
          logger.write(logMaker.setType(MetricsEvent.SHORTCUTS_CHANGED_USER_ID)
                  .setSubtype(mUserId));
          logger.write(logMaker.setType(MetricsEvent.SHORTCUTS_CHANGED_PACKAGE_COUNT)
                  .setSubtype(packageWithShareTargetsCount));
          logger.write(logMaker.setType(MetricsEvent.SHORTCUTS_CHANGED_SHORTCUT_COUNT)
                  .setSubtype(totalSharingShortcutCount));
      }
  }
  