content: /*
   * Copyright (C) 2018 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.wm;
  
  import android.content.ComponentName;
  import android.content.pm.PackageList;
  import android.content.pm.PackageManagerInternal;
  import android.graphics.Rect;
  import android.os.Environment;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.AtomicFile;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.Xml;
  import android.view.DisplayInfo;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.FastXmlSerializer;
  import com.android.server.LocalServices;
  import com.android.server.wm.LaunchParamsController.LaunchParams;
  
  import libcore.io.IoUtils;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlSerializer;
  
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.FileReader;
  import java.io.IOException;
  import java.io.StringWriter;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.function.IntFunction;
  
  /**
   * Persister that saves launch parameters in memory and in storage. It saves the last seen state of
   * tasks key-ed on task's user ID and the activity used to launch the task ({@link
   * TaskRecord#realActivity}) and that's used to determine the launch params when the activity is
   * being launched again in {@link LaunchParamsController}.
   *
   * Need to hold {@link ActivityTaskManagerService#getGlobalLock()} to access this class.
   */
  class LaunchParamsPersister {
      private static final String TAG = "LaunchParamsPersister";
      private static final String LAUNCH_PARAMS_DIRNAME = "launch_params";
      private static final String LAUNCH_PARAMS_FILE_SUFFIX = ".xml";
  
      // Chars below are used to escape the backslash in component name to underscore.
      private static final char ORIGINAL_COMPONENT_SEPARATOR = '/';
      private static final char ESCAPED_COMPONENT_SEPARATOR = '_';
  
      private static final String TAG_LAUNCH_PARAMS = "launch_params";
  
      private final PersisterQueue mPersisterQueue;
      private final ActivityStackSupervisor mSupervisor;
  
      /**
       * A function that takes in user ID and returns a folder to store information of that user. Used
       * to differentiate storage location in test environment and production environment.
       */
      private final IntFunction<File> mUserFolderGetter;
  
      private PackageList mPackageList;
  
      /**
       * A dual layer map that first maps user ID to a secondary map, which maps component name (the
       * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata
       * that are stable across reboots.
       */
      private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mMap =
              new SparseArray<>();
  
      LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor) {
          this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory);
      }
  
      @VisibleForTesting
      LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor,
              IntFunction<File> userFolderGetter) {
          mPersisterQueue = persisterQueue;
          mSupervisor = supervisor;
          mUserFolderGetter = userFolderGetter;
      }
  
      void onSystemReady() {
          PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
          mPackageList = pmi.getPackageList(new PackageListObserver());
      }
  
      void onUnlockUser(int userId) {
          loadLaunchParams(userId);
      }
  
      void onCleanupUser(int userId) {
          mMap.remove(userId);
      }
  
      private void loadLaunchParams(int userId) {
          final List<File> filesToDelete = new ArrayList<>();
          final File launchParamsFolder = getLaunchParamFolder(userId);
          if (!launchParamsFolder.isDirectory()) {
              Slog.i(TAG, "Didn't find launch param folder for user " + userId);
              return;
          }
  
          final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
  
          final File[] paramsFiles = launchParamsFolder.listFiles();
          final ArrayMap<ComponentName, PersistableLaunchParams> map =
                  new ArrayMap<>(paramsFiles.length);
          mMap.put(userId, map);
  
          for (File paramsFile : paramsFiles) {
              if (!paramsFile.isFile()) {
                  Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
                  continue;
              }
              if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
                  Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
                  filesToDelete.add(paramsFile);
                  continue;
              }
              final String paramsFileName = paramsFile.getName();
              final String componentNameString = paramsFileName.substring(
                      0 /* beginIndex */,
                      paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
                      .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
              final ComponentName name = ComponentName.unflattenFromString(
                      componentNameString);
              if (name == null) {
                  Slog.w(TAG, "Unexpected file name: " + paramsFileName);
                  filesToDelete.add(paramsFile);
                  continue;
              }
  
              if (!packages.contains(name.getPackageName())) {
                  // Rare case. PersisterQueue doesn't have a chance to remove files for removed
                  // packages last time.
                  filesToDelete.add(paramsFile);
                  continue;
              }
  
              BufferedReader reader = null;
              try {
                  reader = new BufferedReader(new FileReader(paramsFile));
                  final PersistableLaunchParams params = new PersistableLaunchParams();
                  final XmlPullParser parser = Xml.newPullParser();
                  parser.setInput(reader);
                  int event;
                  while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
                          && event != XmlPullParser.END_TAG) {
                      if (event != XmlPullParser.START_TAG) {
                          continue;
                      }
  
                      final String tagName = parser.getName();
                      if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
                          Slog.w(TAG, "Unexpected tag name: " + tagName);
                          continue;
                      }
  
                      params.restoreFromXml(parser);
                  }
  
                  map.put(name, params);
              } catch (Exception e) {
                  Slog.w(TAG, "Failed to restore launch params for " + name, e);
                  filesToDelete.add(paramsFile);
              } finally {
                  IoUtils.closeQuietly(reader);
              }
          }
  
          if (!filesToDelete.isEmpty()) {
              mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
          }
      }
  
      void saveTask(TaskRecord task) {
          final ComponentName name = task.realActivity;
          final int userId = task.userId;
          PersistableLaunchParams params;
          ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
          if (map == null) {
              map = new ArrayMap<>();
              mMap.put(userId, map);
          }
  
          params = map.get(name);
          if (params == null) {
              params = new PersistableLaunchParams();
              map.put(name, params);
          }
          final boolean changed = saveTaskToLaunchParam(task, params);
  
          if (changed) {
              mPersisterQueue.updateLastOrAddItem(
                      new LaunchParamsWriteQueueItem(userId, name, params),
                      /* flush */ false);
          }
      }
  
      private boolean saveTaskToLaunchParam(TaskRecord task, PersistableLaunchParams params) {
          final ActivityStack stack = task.getStack();
          final int displayId = stack.mDisplayId;
          final ActivityDisplay display =
                  mSupervisor.mRootActivityContainer.getActivityDisplay(displayId);
          final DisplayInfo info = new DisplayInfo();
          display.mDisplay.getDisplayInfo(info);
  
          boolean changed = !Objects.equals(params.mDisplayUniqueId, info.uniqueId);
          params.mDisplayUniqueId = info.uniqueId;
  
          changed |= params.mWindowingMode != stack.getWindowingMode();
          params.mWindowingMode = stack.getWindowingMode();
  
          if (task.mLastNonFullscreenBounds != null) {
              changed |= !Objects.equals(params.mBounds, task.mLastNonFullscreenBounds);
              params.mBounds.set(task.mLastNonFullscreenBounds);
          } else {
              changed |= !params.mBounds.isEmpty();
              params.mBounds.setEmpty();
          }
  
          return changed;
      }
  
      void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams outParams) {
          final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent;
          final int userId = task != null ? task.userId : activity.mUserId;
  
          outParams.reset();
          Map<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
          if (map == null) {
              return;
          }
          final PersistableLaunchParams persistableParams = map.get(name);
  
          if (persistableParams == null) {
              return;
          }
  
          final ActivityDisplay display = mSupervisor.mRootActivityContainer.getActivityDisplay(
                  persistableParams.mDisplayUniqueId);
          if (display != null) {
              outParams.mPreferredDisplayId =  display.mDisplayId;
          }
          outParams.mWindowingMode = persistableParams.mWindowingMode;
          outParams.mBounds.set(persistableParams.mBounds);
      }
  
      void removeRecordForPackage(String packageName) {
          final List<File> fileToDelete = new ArrayList<>();
          for (int i = 0; i < mMap.size(); ++i) {
              int userId = mMap.keyAt(i);
              final File launchParamsFolder = getLaunchParamFolder(userId);
              ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.valueAt(i);
              for (int j = map.size() - 1; j >= 0; --j) {
                  final ComponentName name = map.keyAt(j);
                  if (name.getPackageName().equals(packageName)) {
                      map.removeAt(j);
                      fileToDelete.add(getParamFile(launchParamsFolder, name));
                  }
              }
          }
  
          synchronized (mPersisterQueue) {
              mPersisterQueue.removeItems(
                      item -> item.mComponentName.getPackageName().equals(packageName),
                      LaunchParamsWriteQueueItem.class);
  
              mPersisterQueue.addItem(new CleanUpComponentQueueItem(fileToDelete), true);
          }
      }
  
      private File getParamFile(File launchParamFolder, ComponentName name) {
          final String componentNameString = name.flattenToShortString()
                  .replace(ORIGINAL_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
          return new File(launchParamFolder, componentNameString + LAUNCH_PARAMS_FILE_SUFFIX);
      }
  
      private File getLaunchParamFolder(int userId) {
          final File userFolder = mUserFolderGetter.apply(userId);
          return new File(userFolder, LAUNCH_PARAMS_DIRNAME);
      }
  
      private class PackageListObserver implements PackageManagerInternal.PackageListObserver {
          @Override
          public void onPackageAdded(String packageName, int uid) { }
  
          @Override
          public void onPackageRemoved(String packageName, int uid) {
              removeRecordForPackage(packageName);
          }
      }
  
      private class LaunchParamsWriteQueueItem
              implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
          private final int mUserId;
          private final ComponentName mComponentName;
  
          private PersistableLaunchParams mLaunchParams;
  
          private LaunchParamsWriteQueueItem(int userId, ComponentName componentName,
                  PersistableLaunchParams launchParams) {
              mUserId = userId;
              mComponentName = componentName;
              mLaunchParams = launchParams;
          }
  
          private StringWriter saveParamsToXml() {
              final StringWriter writer = new StringWriter();
              final XmlSerializer serializer = new FastXmlSerializer();
  
              try {
                  serializer.setOutput(writer);
                  serializer.startDocument(/* encoding */ null, /* standalone */ true);
                  serializer.startTag(null, TAG_LAUNCH_PARAMS);
  
                  mLaunchParams.saveToXml(serializer);
  
                  serializer.endTag(null, TAG_LAUNCH_PARAMS);
                  serializer.endDocument();
                  serializer.flush();
  
                  return writer;
              } catch (IOException e) {
                  return null;
              }
          }
  
          @Override
          public void process() {
              final StringWriter writer = saveParamsToXml();
  
              final File launchParamFolder = getLaunchParamFolder(mUserId);
              if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdirs()) {
                  Slog.w(TAG, "Failed to create folder for " + mUserId);
                  return;
              }
  
              final File launchParamFile = getParamFile(launchParamFolder, mComponentName);
              final AtomicFile atomicFile = new AtomicFile(launchParamFile);
  
              FileOutputStream stream = null;
              try {
                  stream = atomicFile.startWrite();
                  stream.write(writer.toString().getBytes());
              } catch (Exception e) {
                  Slog.e(TAG, "Failed to write param file for " + mComponentName, e);
                  if (stream != null) {
                      atomicFile.failWrite(stream);
                  }
                  return;
              }
              atomicFile.finishWrite(stream);
          }
  
          @Override
          public boolean matches(LaunchParamsWriteQueueItem item) {
              return mUserId == item.mUserId && mComponentName.equals(item.mComponentName);
          }
  
          @Override
          public void updateFrom(LaunchParamsWriteQueueItem item) {
              mLaunchParams = item.mLaunchParams;
          }
      }
  
      private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
          private final List<File> mComponentFiles;
  
          private CleanUpComponentQueueItem(List<File> componentFiles) {
              mComponentFiles = componentFiles;
          }
  
          @Override
          public void process() {
              for (File file : mComponentFiles) {
                  if (!file.delete()) {
                      Slog.w(TAG, "Failed to delete " + file.getAbsolutePath());
                  }
              }
          }
      }
  
      private class PersistableLaunchParams {
          private static final String ATTR_WINDOWING_MODE = "windowing_mode";
          private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
          private static final String ATTR_BOUNDS = "bounds";
  
          /** The bounds within the parent container. */
          final Rect mBounds = new Rect();
  
          /** The unique id of the display the {@link TaskRecord} would prefer to be on. */
          String mDisplayUniqueId;
  
          /** The windowing mode to be in. */
          int mWindowingMode;
  
          void saveToXml(XmlSerializer serializer) throws IOException {
              serializer.attribute(null, ATTR_DISPLAY_UNIQUE_ID, mDisplayUniqueId);
              serializer.attribute(null, ATTR_WINDOWING_MODE,
                      Integer.toString(mWindowingMode));
              serializer.attribute(null, ATTR_BOUNDS, mBounds.flattenToString());
          }
  
          void restoreFromXml(XmlPullParser parser) {
              for (int i = 0; i < parser.getAttributeCount(); ++i) {
                  final String attrValue = parser.getAttributeValue(i);
                  switch (parser.getAttributeName(i)) {
                      case ATTR_DISPLAY_UNIQUE_ID:
                          mDisplayUniqueId = attrValue;
                          break;
                      case ATTR_WINDOWING_MODE:
                          mWindowingMode = Integer.parseInt(attrValue);
                          break;
                      case ATTR_BOUNDS: {
                          final Rect bounds = Rect.unflattenFromString(attrValue);
                          if (bounds != null) {
                              mBounds.set(bounds);
                          }
                          break;
                      }
                  }
              }
          }
  
          @Override
          public String toString() {
              final StringBuilder builder = new StringBuilder("PersistableLaunchParams{");
              builder.append("windowingMode=" + mWindowingMode);
              builder.append(" displayUniqueId=" + mDisplayUniqueId);
              builder.append(" bounds=" + mBounds);
              builder.append(" }");
              return builder.toString();
          }
      }
  }
  