1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import android.annotation.SuppressLint; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 import android.os.UserHandle; 23 import android.os.storage.StorageVolume; 24 import android.provider.MediaStore; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 29 import com.android.modules.utils.build.SdkLevel; 30 31 import java.io.File; 32 import java.util.Objects; 33 34 /** 35 * MediaVolume is a MediaProvider-internal representation of a storage volume. 36 * 37 * Before MediaVolume, volumes inside MediaProvider were represented by their name; 38 * but now that MediaProvider handles volumes on behalf on multiple users, the name of a volume 39 * might no longer be unique. So MediaVolume holds both a name and a user. The user may be 40 * null on volumes without an owner (eg public volumes). 41 * 42 * In addition to that, we keep the path and ID of the volume cached in here as well 43 * for easy access. 44 */ 45 public final class MediaVolume implements Parcelable { 46 /** 47 * Name of the volume. 48 */ 49 private final @NonNull String mName; 50 51 /** 52 * User to which the volume belongs to; might be null in case of public volumes. 53 */ 54 private final @Nullable UserHandle mUser; 55 56 /** 57 * Path on which the volume is mounted. 58 */ 59 private final @Nullable File mPath; 60 61 /** 62 * Unique ID of the volume; eg "external;0" 63 */ 64 private final @Nullable String mId; 65 66 /** 67 * Whether the volume is managed from outside Android. 68 */ 69 private final boolean mExternallyManaged; 70 71 /** 72 * Whether the volume is public. 73 */ 74 private final boolean mPublicVolume; 75 getName()76 public @NonNull String getName() { 77 return mName; 78 } 79 getUser()80 public @Nullable UserHandle getUser() { 81 return mUser; 82 } 83 getPath()84 public @Nullable File getPath() { 85 return mPath; 86 } 87 getId()88 public @Nullable String getId() { 89 return mId; 90 } 91 isExternallyManaged()92 private boolean isExternallyManaged() { 93 return mExternallyManaged; 94 } 95 isPublicVolume()96 public boolean isPublicVolume() { 97 return mPublicVolume; 98 } 99 MediaVolume(@onNull String name, UserHandle user, File path, String id, boolean externallyManaged, boolean mPublicVolume)100 private MediaVolume (@NonNull String name, UserHandle user, File path, String id, 101 boolean externallyManaged, boolean mPublicVolume) { 102 this.mName = name; 103 this.mUser = user; 104 this.mPath = path; 105 this.mId = id; 106 this.mExternallyManaged = externallyManaged; 107 this.mPublicVolume = mPublicVolume; 108 } 109 MediaVolume(Parcel in)110 private MediaVolume (Parcel in) { 111 this.mName = in.readString(); 112 this.mUser = in.readParcelable(null); 113 this.mPath = new File(in.readString()); 114 this.mId = in.readString(); 115 this.mExternallyManaged = in.readInt() != 0; 116 this.mPublicVolume = in.readInt() != 0; 117 } 118 119 @Override equals(Object obj)120 public boolean equals(Object obj) { 121 if (this == obj) return true; 122 if (obj == null || getClass() != obj.getClass()) return false; 123 MediaVolume that = (MediaVolume) obj; 124 // We consciously don't compare the path, because: 125 // 1. On unmount events, the returned path for StorageVolumes is 126 // 'null', and different from a mounted volume. 127 // 2. A volume with a certain ID should never be mounted in two different paths, anyway 128 return Objects.equals(mName, that.mName) && 129 Objects.equals(mUser, that.mUser) && 130 Objects.equals(mId, that.mId) && 131 (mExternallyManaged == that.mExternallyManaged); 132 } 133 134 @Override hashCode()135 public int hashCode() { 136 return Objects.hash(mName, mUser, mId, mExternallyManaged); 137 } 138 isVisibleToUser(UserHandle user)139 public boolean isVisibleToUser(UserHandle user) { 140 return mUser == null || user.equals(mUser); 141 } 142 143 /** 144 * Should skip default dir creating for externally managed volumes and for unreliable 145 * public volumes. 146 */ shouldSkipDefaultDirCreation()147 public boolean shouldSkipDefaultDirCreation() { 148 return isExternallyManaged() || isUnreliablePublicVolume(); 149 } 150 isUnreliablePublicVolume()151 private boolean isUnreliablePublicVolume() { 152 return isPublicVolume() && getPath() != null 153 && getPath().getAbsolutePath().startsWith("/mnt/"); 154 } 155 156 /** 157 * Adding NewApi Suppress Lint to fix some build errors after making 158 * {@link StorageVolume#getOwner()} a public Api 159 */ 160 // TODO(b/213658045) : Remove this once the related changes are submitted. 161 @SuppressLint("NewApi") 162 @NonNull fromStorageVolume(StorageVolume storageVolume)163 public static MediaVolume fromStorageVolume(StorageVolume storageVolume) { 164 String name = storageVolume.getMediaStoreVolumeName(); 165 UserHandle user = storageVolume.getOwner(); 166 File path = storageVolume.getDirectory(); 167 String id = storageVolume.getId(); 168 boolean externallyManaged = 169 SdkLevel.isAtLeastT() ? storageVolume.isExternallyManaged() : false; 170 boolean publicVolume = !externallyManaged && !storageVolume.isPrimary(); 171 return new MediaVolume(name, user, path, id, externallyManaged, publicVolume); 172 } 173 fromInternal()174 public static MediaVolume fromInternal() { 175 String name = MediaStore.VOLUME_INTERNAL; 176 return new MediaVolume(name, null, null, null, false, false); 177 } 178 179 @Override describeContents()180 public int describeContents() { 181 return 0; 182 } 183 184 @Override writeToParcel(Parcel dest, int flags)185 public void writeToParcel(Parcel dest, int flags) { 186 dest.writeString(mName); 187 dest.writeParcelable(mUser, flags); 188 dest.writeString(mPath.toString()); 189 dest.writeString(mId); 190 dest.writeInt(mExternallyManaged ? 1 : 0); 191 dest.writeInt(mPublicVolume ? 1 : 0); 192 } 193 194 @Override toString()195 public String toString() { 196 return "MediaVolume name: [" + mName + "] id: [" + mId + "] user: [" + mUser + "] path: [" 197 + mPath + "] externallyManaged: [" + mExternallyManaged + "] mPublicVolume: [" 198 + mPublicVolume + "]"; 199 } 200 201 public static final @android.annotation.NonNull Creator<MediaVolume> CREATOR 202 = new Creator<MediaVolume>() { 203 @Override 204 public MediaVolume createFromParcel(Parcel in) { 205 return new MediaVolume(in); 206 } 207 208 @Override 209 public MediaVolume[] newArray(int size) { 210 return new MediaVolume[size]; 211 } 212 }; 213 } 214