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.devicepolicy;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.admin.DevicePolicyManager;
  import android.content.ComponentName;
  import android.os.Environment;
  import android.text.TextUtils;
  import android.util.AtomicFile;
  import android.util.Slog;
  import android.util.Xml;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.FastXmlSerializer;
  import com.android.internal.util.Preconditions;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlSerializer;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  
  /**
   * Handles reading and writing of the owner transfer metadata file.
   *
   * Before we perform a device or profile owner transfer, we save this xml file with information
   * about the current admin, target admin, user id and admin type (device owner or profile owner).
   * After {@link DevicePolicyManager#transferOwnership} completes, we delete the file. If after
   * device boot the file is still there, this indicates that the transfer was interrupted by a
   * reboot.
   *
   * Note that this class is not thread safe.
   */
  class TransferOwnershipMetadataManager {
      final static String ADMIN_TYPE_DEVICE_OWNER = "device-owner";
      final static String ADMIN_TYPE_PROFILE_OWNER = "profile-owner";
      @VisibleForTesting
      final static String TAG_USER_ID = "user-id";
      @VisibleForTesting
      final static String TAG_SOURCE_COMPONENT = "source-component";
      @VisibleForTesting
      final static String TAG_TARGET_COMPONENT = "target-component";
      @VisibleForTesting
      final static String TAG_ADMIN_TYPE = "admin-type";
      private final static String TAG = TransferOwnershipMetadataManager.class.getName();
      public static final String OWNER_TRANSFER_METADATA_XML = "owner-transfer-metadata.xml";
  
      private final Injector mInjector;
  
      TransferOwnershipMetadataManager() {
          this(new Injector());
      }
  
      @VisibleForTesting
      TransferOwnershipMetadataManager(Injector injector) {
          mInjector = injector;
      }
  
      boolean saveMetadataFile(Metadata params) {
          final File transferOwnershipMetadataFile = new File(mInjector.getOwnerTransferMetadataDir(),
                  OWNER_TRANSFER_METADATA_XML);
          final AtomicFile atomicFile = new AtomicFile(transferOwnershipMetadataFile);
          FileOutputStream stream = null;
          try {
              stream = atomicFile.startWrite();
              final XmlSerializer serializer = new FastXmlSerializer();
              serializer.setOutput(stream, StandardCharsets.UTF_8.name());
              serializer.startDocument(null, true);
              insertSimpleTag(serializer, TAG_USER_ID, Integer.toString(params.userId));
              insertSimpleTag(serializer,
                      TAG_SOURCE_COMPONENT, params.sourceComponent.flattenToString());
              insertSimpleTag(serializer,
                      TAG_TARGET_COMPONENT, params.targetComponent.flattenToString());
              insertSimpleTag(serializer, TAG_ADMIN_TYPE, params.adminType);
              serializer.endDocument();
              atomicFile.finishWrite(stream);
              return true;
          } catch (IOException e) {
              Slog.e(TAG, "Caught exception while trying to save Owner Transfer "
                      + "Params to file " + transferOwnershipMetadataFile, e);
              transferOwnershipMetadataFile.delete();
              atomicFile.failWrite(stream);
          }
          return false;
      }
  
      private void insertSimpleTag(XmlSerializer serializer, String tagName, String value)
              throws IOException {
          serializer.startTag(null, tagName);
          serializer.text(value);
          serializer.endTag(null, tagName);
      }
  
      @Nullable
      Metadata loadMetadataFile() {
          final File transferOwnershipMetadataFile =
                  new File(mInjector.getOwnerTransferMetadataDir(), OWNER_TRANSFER_METADATA_XML);
          if (!transferOwnershipMetadataFile.exists()) {
              return null;
          }
          Slog.d(TAG, "Loading TransferOwnershipMetadataManager from "
                  + transferOwnershipMetadataFile);
          try (FileInputStream stream = new FileInputStream(transferOwnershipMetadataFile)) {
              final XmlPullParser parser = Xml.newPullParser();
              parser.setInput(stream, null);
              return parseMetadataFile(parser);
          } catch (IOException | XmlPullParserException | IllegalArgumentException e) {
              Slog.e(TAG, "Caught exception while trying to load the "
                      + "owner transfer params from file " + transferOwnershipMetadataFile, e);
          }
          return null;
      }
  
      private Metadata parseMetadataFile(XmlPullParser parser)
              throws XmlPullParserException, IOException {
          int type;
          final int outerDepth = parser.getDepth();
          int userId = 0;
          String adminComponent = null;
          String targetComponent = null;
          String adminType = null;
          while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                  && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
              if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                  continue;
              }
              switch (parser.getName()) {
                  case TAG_USER_ID:
                      parser.next();
                      userId = Integer.parseInt(parser.getText());
                      break;
                  case TAG_TARGET_COMPONENT:
                      parser.next();
                      targetComponent = parser.getText();
                      break;
                  case TAG_SOURCE_COMPONENT:
                      parser.next();
                      adminComponent = parser.getText();
                      break;
                  case TAG_ADMIN_TYPE:
                      parser.next();
                      adminType = parser.getText();
                      break;
              }
          }
          return new Metadata(adminComponent, targetComponent, userId, adminType);
      }
  
      void deleteMetadataFile() {
          new File(mInjector.getOwnerTransferMetadataDir(), OWNER_TRANSFER_METADATA_XML).delete();
      }
  
      boolean metadataFileExists() {
          return new File(mInjector.getOwnerTransferMetadataDir(),
                  OWNER_TRANSFER_METADATA_XML).exists();
      }
  
      static class Metadata {
          final int userId;
          final ComponentName sourceComponent;
          final ComponentName targetComponent;
          final String adminType;
  
          Metadata(@NonNull ComponentName sourceComponent, @NonNull ComponentName targetComponent,
                  @NonNull int userId, @NonNull String adminType) {
              this.sourceComponent = sourceComponent;
              this.targetComponent = targetComponent;
              Preconditions.checkNotNull(sourceComponent);
              Preconditions.checkNotNull(targetComponent);
              Preconditions.checkStringNotEmpty(adminType);
              this.userId = userId;
              this.adminType = adminType;
          }
  
          Metadata(@NonNull String flatSourceComponent, @NonNull String flatTargetComponent,
                  @NonNull int userId, @NonNull String adminType) {
              this(unflattenComponentUnchecked(flatSourceComponent),
                      unflattenComponentUnchecked(flatTargetComponent), userId, adminType);
          }
  
          private static ComponentName unflattenComponentUnchecked(String flatComponent) {
              Preconditions.checkNotNull(flatComponent);
              return ComponentName.unflattenFromString(flatComponent);
          }
  
          @Override
          public boolean equals(Object obj) {
              if (!(obj instanceof Metadata)) {
                  return false;
              }
              Metadata params = (Metadata) obj;
  
              return userId == params.userId
                      && sourceComponent.equals(params.sourceComponent)
                      && targetComponent.equals(params.targetComponent)
                      && TextUtils.equals(adminType, params.adminType);
          }
  
          @Override
          public int hashCode() {
              int hashCode = 1;
              hashCode = 31 * hashCode + userId;
              hashCode = 31 * hashCode + sourceComponent.hashCode();
              hashCode = 31 * hashCode + targetComponent.hashCode();
              hashCode = 31 * hashCode + adminType.hashCode();
              return hashCode;
          }
      }
  
      @VisibleForTesting
      static class Injector {
          public File getOwnerTransferMetadataDir() {
              return Environment.getDataSystemDirectory();
          }
      }
  }
  