content: /*
   * Copyright (C) 2015 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 android.os.storage;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.content.Intent;
  import android.content.res.Resources;
  import android.net.Uri;
  import android.os.Environment;
  import android.os.IVold;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.UserHandle;
  import android.provider.DocumentsContract;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.DebugUtils;
  import android.util.SparseArray;
  import android.util.SparseIntArray;
  
  import com.android.internal.R;
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.internal.util.Preconditions;
  
  import java.io.CharArrayWriter;
  import java.io.File;
  import java.util.Comparator;
  import java.util.Locale;
  import java.util.Objects;
  
  /**
   * Information about a storage volume that may be mounted. A volume may be a
   * partition on a physical {@link DiskInfo}, an emulated volume above some other
   * storage medium, or a standalone container like an ASEC or OBB.
   * <p>
   * Volumes may be mounted with various flags:
   * <ul>
   * <li>{@link #MOUNT_FLAG_PRIMARY} means the volume provides primary external
   * storage, historically found at {@code /sdcard}.
   * <li>{@link #MOUNT_FLAG_VISIBLE} means the volume is visible to third-party
   * apps for direct filesystem access. The system should send out relevant
   * storage broadcasts and index any media on visible volumes. Visible volumes
   * are considered a more stable part of the device, which is why we take the
   * time to index them. In particular, transient volumes like USB OTG devices
   * <em>should not</em> be marked as visible; their contents should be surfaced
   * to apps through the Storage Access Framework.
   * </ul>
   *
   * @hide
   */
  public class VolumeInfo implements Parcelable {
      public static final String ACTION_VOLUME_STATE_CHANGED =
              "android.os.storage.action.VOLUME_STATE_CHANGED";
      public static final String EXTRA_VOLUME_ID =
              "android.os.storage.extra.VOLUME_ID";
      public static final String EXTRA_VOLUME_STATE =
              "android.os.storage.extra.VOLUME_STATE";
  
      /** Stub volume representing internal private storage */
      public static final String ID_PRIVATE_INTERNAL = "private";
      /** Real volume representing internal emulated storage */
      public static final String ID_EMULATED_INTERNAL = "emulated";
  
      @UnsupportedAppUsage
      public static final int TYPE_PUBLIC = IVold.VOLUME_TYPE_PUBLIC;
      public static final int TYPE_PRIVATE = IVold.VOLUME_TYPE_PRIVATE;
      @UnsupportedAppUsage
      public static final int TYPE_EMULATED = IVold.VOLUME_TYPE_EMULATED;
      public static final int TYPE_ASEC = IVold.VOLUME_TYPE_ASEC;
      public static final int TYPE_OBB = IVold.VOLUME_TYPE_OBB;
      public static final int TYPE_STUB = IVold.VOLUME_TYPE_STUB;
  
      public static final int STATE_UNMOUNTED = IVold.VOLUME_STATE_UNMOUNTED;
      public static final int STATE_CHECKING = IVold.VOLUME_STATE_CHECKING;
      public static final int STATE_MOUNTED = IVold.VOLUME_STATE_MOUNTED;
      public static final int STATE_MOUNTED_READ_ONLY = IVold.VOLUME_STATE_MOUNTED_READ_ONLY;
      public static final int STATE_FORMATTING = IVold.VOLUME_STATE_FORMATTING;
      public static final int STATE_EJECTING = IVold.VOLUME_STATE_EJECTING;
      public static final int STATE_UNMOUNTABLE = IVold.VOLUME_STATE_UNMOUNTABLE;
      public static final int STATE_REMOVED = IVold.VOLUME_STATE_REMOVED;
      public static final int STATE_BAD_REMOVAL = IVold.VOLUME_STATE_BAD_REMOVAL;
  
      public static final int MOUNT_FLAG_PRIMARY = IVold.MOUNT_FLAG_PRIMARY;
      public static final int MOUNT_FLAG_VISIBLE = IVold.MOUNT_FLAG_VISIBLE;
  
      private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
      private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
      private static SparseIntArray sStateToDescrip = new SparseIntArray();
  
      private static final Comparator<VolumeInfo>
              sDescriptionComparator = new Comparator<VolumeInfo>() {
          @Override
          public int compare(VolumeInfo lhs, VolumeInfo rhs) {
              if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(lhs.getId())) {
                  return -1;
              } else if (lhs.getDescription() == null) {
                  return 1;
              } else if (rhs.getDescription() == null) {
                  return -1;
              } else {
                  return lhs.getDescription().compareTo(rhs.getDescription());
              }
          }
      };
  
      static {
          sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTED, Environment.MEDIA_UNMOUNTED);
          sStateToEnvironment.put(VolumeInfo.STATE_CHECKING, Environment.MEDIA_CHECKING);
          sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED, Environment.MEDIA_MOUNTED);
          sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, Environment.MEDIA_MOUNTED_READ_ONLY);
          sStateToEnvironment.put(VolumeInfo.STATE_FORMATTING, Environment.MEDIA_UNMOUNTED);
          sStateToEnvironment.put(VolumeInfo.STATE_EJECTING, Environment.MEDIA_EJECTING);
          sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTABLE, Environment.MEDIA_UNMOUNTABLE);
          sStateToEnvironment.put(VolumeInfo.STATE_REMOVED, Environment.MEDIA_REMOVED);
          sStateToEnvironment.put(VolumeInfo.STATE_BAD_REMOVAL, Environment.MEDIA_BAD_REMOVAL);
  
          sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTED, Intent.ACTION_MEDIA_UNMOUNTED);
          sEnvironmentToBroadcast.put(Environment.MEDIA_CHECKING, Intent.ACTION_MEDIA_CHECKING);
          sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED, Intent.ACTION_MEDIA_MOUNTED);
          sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED_READ_ONLY, Intent.ACTION_MEDIA_MOUNTED);
          sEnvironmentToBroadcast.put(Environment.MEDIA_EJECTING, Intent.ACTION_MEDIA_EJECT);
          sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTABLE, Intent.ACTION_MEDIA_UNMOUNTABLE);
          sEnvironmentToBroadcast.put(Environment.MEDIA_REMOVED, Intent.ACTION_MEDIA_REMOVED);
          sEnvironmentToBroadcast.put(Environment.MEDIA_BAD_REMOVAL, Intent.ACTION_MEDIA_BAD_REMOVAL);
  
          sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTED, R.string.ext_media_status_unmounted);
          sStateToDescrip.put(VolumeInfo.STATE_CHECKING, R.string.ext_media_status_checking);
          sStateToDescrip.put(VolumeInfo.STATE_MOUNTED, R.string.ext_media_status_mounted);
          sStateToDescrip.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, R.string.ext_media_status_mounted_ro);
          sStateToDescrip.put(VolumeInfo.STATE_FORMATTING, R.string.ext_media_status_formatting);
          sStateToDescrip.put(VolumeInfo.STATE_EJECTING, R.string.ext_media_status_ejecting);
          sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTABLE, R.string.ext_media_status_unmountable);
          sStateToDescrip.put(VolumeInfo.STATE_REMOVED, R.string.ext_media_status_removed);
          sStateToDescrip.put(VolumeInfo.STATE_BAD_REMOVAL, R.string.ext_media_status_bad_removal);
      }
  
      /** vold state */
      public final String id;
      @UnsupportedAppUsage
      public final int type;
      @UnsupportedAppUsage
      public final DiskInfo disk;
      public final String partGuid;
      public int mountFlags = 0;
      public int mountUserId = UserHandle.USER_NULL;
      @UnsupportedAppUsage
      public int state = STATE_UNMOUNTED;
      public String fsType;
      @UnsupportedAppUsage
      public String fsUuid;
      @UnsupportedAppUsage
      public String fsLabel;
      @UnsupportedAppUsage
      public String path;
      @UnsupportedAppUsage
      public String internalPath;
  
      public VolumeInfo(String id, int type, DiskInfo disk, String partGuid) {
          this.id = Preconditions.checkNotNull(id);
          this.type = type;
          this.disk = disk;
          this.partGuid = partGuid;
      }
  
      @UnsupportedAppUsage
      public VolumeInfo(Parcel parcel) {
          id = parcel.readString();
          type = parcel.readInt();
          if (parcel.readInt() != 0) {
              disk = DiskInfo.CREATOR.createFromParcel(parcel);
          } else {
              disk = null;
          }
          partGuid = parcel.readString();
          mountFlags = parcel.readInt();
          mountUserId = parcel.readInt();
          state = parcel.readInt();
          fsType = parcel.readString();
          fsUuid = parcel.readString();
          fsLabel = parcel.readString();
          path = parcel.readString();
          internalPath = parcel.readString();
      }
  
      @UnsupportedAppUsage
      public static @NonNull String getEnvironmentForState(int state) {
          final String envState = sStateToEnvironment.get(state);
          if (envState != null) {
              return envState;
          } else {
              return Environment.MEDIA_UNKNOWN;
          }
      }
  
      public static @Nullable String getBroadcastForEnvironment(String envState) {
          return sEnvironmentToBroadcast.get(envState);
      }
  
      public static @Nullable String getBroadcastForState(int state) {
          return getBroadcastForEnvironment(getEnvironmentForState(state));
      }
  
      public static @NonNull Comparator<VolumeInfo> getDescriptionComparator() {
          return sDescriptionComparator;
      }
  
      @UnsupportedAppUsage
      public @NonNull String getId() {
          return id;
      }
  
      @UnsupportedAppUsage
      public @Nullable DiskInfo getDisk() {
          return disk;
      }
  
      @UnsupportedAppUsage
      public @Nullable String getDiskId() {
          return (disk != null) ? disk.id : null;
      }
  
      @UnsupportedAppUsage
      public int getType() {
          return type;
      }
  
      @UnsupportedAppUsage
      public int getState() {
          return state;
      }
  
      public int getStateDescription() {
          return sStateToDescrip.get(state, 0);
      }
  
      @UnsupportedAppUsage
      public @Nullable String getFsUuid() {
          return fsUuid;
      }
  
      public @Nullable String getNormalizedFsUuid() {
          return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null;
      }
  
      @UnsupportedAppUsage
      public int getMountUserId() {
          return mountUserId;
      }
  
      @UnsupportedAppUsage
      public @Nullable String getDescription() {
          if (ID_PRIVATE_INTERNAL.equals(id) || ID_EMULATED_INTERNAL.equals(id)) {
              return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
          } else if (!TextUtils.isEmpty(fsLabel)) {
              return fsLabel;
          } else {
              return null;
          }
      }
  
      @UnsupportedAppUsage
      public boolean isMountedReadable() {
          return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY;
      }
  
      @UnsupportedAppUsage
      public boolean isMountedWritable() {
          return state == STATE_MOUNTED;
      }
  
      @UnsupportedAppUsage
      public boolean isPrimary() {
          return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
      }
  
      @UnsupportedAppUsage
      public boolean isPrimaryPhysical() {
          return isPrimary() && (getType() == TYPE_PUBLIC);
      }
  
      @UnsupportedAppUsage
      public boolean isVisible() {
          return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
      }
  
      public boolean isVisibleForUser(int userId) {
          if ((type == TYPE_PUBLIC || type == TYPE_STUB) && mountUserId == userId) {
              return isVisible();
          } else if (type == TYPE_EMULATED) {
              return isVisible();
          } else {
              return false;
          }
      }
  
      public boolean isVisibleForRead(int userId) {
          return isVisibleForUser(userId);
      }
  
      @UnsupportedAppUsage
      public boolean isVisibleForWrite(int userId) {
          return isVisibleForUser(userId);
      }
  
      @UnsupportedAppUsage
      public File getPath() {
          return (path != null) ? new File(path) : null;
      }
  
      @UnsupportedAppUsage
      public File getInternalPath() {
          return (internalPath != null) ? new File(internalPath) : null;
      }
  
      @UnsupportedAppUsage
      public File getPathForUser(int userId) {
          if (path == null) {
              return null;
          } else if (type == TYPE_PUBLIC || type == TYPE_STUB) {
              return new File(path);
          } else if (type == TYPE_EMULATED) {
              return new File(path, Integer.toString(userId));
          } else {
              return null;
          }
      }
  
      /**
       * Path which is accessible to apps holding
       * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}.
       */
      @UnsupportedAppUsage
      public File getInternalPathForUser(int userId) {
          if (path == null) {
              return null;
          } else if (type == TYPE_PUBLIC || type == TYPE_STUB) {
              // TODO: plumb through cleaner path from vold
              return new File(path.replace("/storage/", "/mnt/media_rw/"));
          } else {
              return getPathForUser(userId);
          }
      }
  
      @UnsupportedAppUsage
      public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
          final StorageManager storage = context.getSystemService(StorageManager.class);
  
          final boolean removable;
          final boolean emulated;
          final boolean allowMassStorage = false;
          final String envState = reportUnmounted
                  ? Environment.MEDIA_UNMOUNTED : getEnvironmentForState(state);
  
          File userPath = getPathForUser(userId);
          if (userPath == null) {
              userPath = new File("/dev/null");
          }
          File internalPath = getInternalPathForUser(userId);
          if (internalPath == null) {
              internalPath = new File("/dev/null");
          }
  
          String description = null;
          String derivedFsUuid = fsUuid;
          long maxFileSize = 0;
  
          if (type == TYPE_EMULATED) {
              emulated = true;
  
              final VolumeInfo privateVol = storage.findPrivateForEmulated(this);
              if (privateVol != null) {
                  description = storage.getBestVolumeDescription(privateVol);
                  derivedFsUuid = privateVol.fsUuid;
              }
  
              if (ID_EMULATED_INTERNAL.equals(id)) {
                  removable = false;
              } else {
                  removable = true;
              }
  
          } else if (type == TYPE_PUBLIC || type == TYPE_STUB) {
              emulated = false;
              removable = true;
  
              description = storage.getBestVolumeDescription(this);
  
              if ("vfat".equals(fsType)) {
                  maxFileSize = 4294967295L;
              }
  
          } else {
              throw new IllegalStateException("Unexpected volume type " + type);
          }
  
          if (description == null) {
              description = context.getString(android.R.string.unknownName);
          }
  
          return new StorageVolume(id, userPath, internalPath, description, isPrimary(), removable,
                  emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
                  derivedFsUuid, envState);
      }
  
      @UnsupportedAppUsage
      public static int buildStableMtpStorageId(String fsUuid) {
          if (TextUtils.isEmpty(fsUuid)) {
              return StorageVolume.STORAGE_ID_INVALID;
          } else {
              int hash = 0;
              for (int i = 0; i < fsUuid.length(); ++i) {
                  hash = 31 * hash + fsUuid.charAt(i);
              }
              hash = (hash ^ (hash << 16)) & 0xffff0000;
              // Work around values that the spec doesn't allow, or that we've
              // reserved for primary
              if (hash == 0x00000000) hash = 0x00020000;
              if (hash == 0x00010000) hash = 0x00020000;
              if (hash == 0xffff0000) hash = 0xfffe0000;
              return hash | 0x0001;
          }
      }
  
      // TODO: avoid this layering violation
      private static final String DOCUMENT_AUTHORITY = "com.android.externalstorage.documents";
      private static final String DOCUMENT_ROOT_PRIMARY_EMULATED = "primary";
  
      /**
       * Build an intent to browse the contents of this volume. Only valid for
       * {@link #TYPE_EMULATED} or {@link #TYPE_PUBLIC}.
       */
      @UnsupportedAppUsage
      public @Nullable Intent buildBrowseIntent() {
          return buildBrowseIntentForUser(UserHandle.myUserId());
      }
  
      public @Nullable Intent buildBrowseIntentForUser(int userId) {
          final Uri uri;
          if ((type == VolumeInfo.TYPE_PUBLIC || type == VolumeInfo.TYPE_STUB)
                  && mountUserId == userId) {
              uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, fsUuid);
          } else if (type == VolumeInfo.TYPE_EMULATED && isPrimary()) {
              uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY,
                      DOCUMENT_ROOT_PRIMARY_EMULATED);
          } else {
              return null;
          }
  
          final Intent intent = new Intent(Intent.ACTION_VIEW);
          intent.addCategory(Intent.CATEGORY_DEFAULT);
          intent.setDataAndType(uri, DocumentsContract.Root.MIME_TYPE_ITEM);
  
          // note that docsui treats this as *force* show advanced. So sending
          // false permits advanced to be shown based on user preferences.
          intent.putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, isPrimary());
          return intent;
      }
  
      @Override
      public String toString() {
          final CharArrayWriter writer = new CharArrayWriter();
          dump(new IndentingPrintWriter(writer, "    ", 80));
          return writer.toString();
      }
  
      public void dump(IndentingPrintWriter pw) {
          pw.println("VolumeInfo{" + id + "}:");
          pw.increaseIndent();
          pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
          pw.printPair("diskId", getDiskId());
          pw.printPair("partGuid", partGuid);
          pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
          pw.printPair("mountUserId", mountUserId);
          pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
          pw.println();
          pw.printPair("fsType", fsType);
          pw.printPair("fsUuid", fsUuid);
          pw.printPair("fsLabel", fsLabel);
          pw.println();
          pw.printPair("path", path);
          pw.printPair("internalPath", internalPath);
          pw.decreaseIndent();
          pw.println();
      }
  
      @Override
      public VolumeInfo clone() {
          final Parcel temp = Parcel.obtain();
          try {
              writeToParcel(temp, 0);
              temp.setDataPosition(0);
              return CREATOR.createFromParcel(temp);
          } finally {
              temp.recycle();
          }
      }
  
      @Override
      public boolean equals(Object o) {
          if (o instanceof VolumeInfo) {
              return Objects.equals(id, ((VolumeInfo) o).id);
          } else {
              return false;
          }
      }
  
      @Override
      public int hashCode() {
          return id.hashCode();
      }
  
      @UnsupportedAppUsage
      public static final @android.annotation.NonNull Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() {
          @Override
          public VolumeInfo createFromParcel(Parcel in) {
              return new VolumeInfo(in);
          }
  
          @Override
          public VolumeInfo[] newArray(int size) {
              return new VolumeInfo[size];
          }
      };
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel parcel, int flags) {
          parcel.writeString(id);
          parcel.writeInt(type);
          if (disk != null) {
              parcel.writeInt(1);
              disk.writeToParcel(parcel, flags);
          } else {
              parcel.writeInt(0);
          }
          parcel.writeString(partGuid);
          parcel.writeInt(mountFlags);
          parcel.writeInt(mountUserId);
          parcel.writeInt(state);
          parcel.writeString(fsType);
          parcel.writeString(fsUuid);
          parcel.writeString(fsLabel);
          parcel.writeString(path);
          parcel.writeString(internalPath);
      }
  }
  