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