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 static com.android.providers.media.util.Logging.TAG;
20 
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.UserHandle;
24 import android.os.UserManager;
25 import android.os.storage.StorageManager;
26 import android.os.storage.StorageVolume;
27 import android.provider.MediaStore;
28 import android.util.ArrayMap;
29 import android.util.ArraySet;
30 import android.util.Log;
31 import android.util.LongSparseArray;
32 
33 import androidx.annotation.GuardedBy;
34 import androidx.annotation.NonNull;
35 
36 import com.android.providers.media.util.FileUtils;
37 import com.android.providers.media.util.UserCache;
38 
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.PrintWriter;
42 import java.util.ArrayList;
43 import java.util.Collection;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Set;
47 
48 /**
49  * The VolumeCache class keeps track of all the volumes that are available,
50  * as well as their scan paths.
51  */
52 public class VolumeCache {
53     private final Context mContext;
54 
55     private final Object mLock = new Object();
56 
57     private final UserManager mUserManager;
58     private final UserCache mUserCache;
59 
60     @GuardedBy("mLock")
61     private final ArrayList<MediaVolume> mExternalVolumes = new ArrayList<>();
62 
63     @GuardedBy("mLock")
64     private final Map<MediaVolume, Collection<File>> mCachedVolumeScanPaths = new ArrayMap<>();
65 
66     @GuardedBy("mLock")
67     private Collection<File> mCachedInternalScanPaths;
68 
VolumeCache(Context context, UserCache userCache)69     public VolumeCache(Context context, UserCache userCache) {
70         mContext = context;
71         mUserManager = context.getSystemService(UserManager.class);
72         mUserCache = userCache;
73     }
74 
getExternalVolumes()75     public @NonNull List<MediaVolume> getExternalVolumes() {
76         synchronized(mLock) {
77             return new ArrayList<>(mExternalVolumes);
78         }
79     }
80 
getExternalVolumeNames()81     public @NonNull Set<String> getExternalVolumeNames() {
82         synchronized (mLock) {
83             ArraySet<String> volNames = new ArraySet<String>();
84             for (MediaVolume vol : mExternalVolumes) {
85                 volNames.add(vol.getName());
86             }
87             return volNames;
88         }
89     }
90 
findVolume(@onNull String volumeName, @NonNull UserHandle user)91     public @NonNull MediaVolume findVolume(@NonNull String volumeName, @NonNull UserHandle user)
92             throws FileNotFoundException {
93         synchronized (mLock) {
94             for (MediaVolume vol : mExternalVolumes) {
95                 if (vol.getName().equals(volumeName) && vol.isVisibleToUser(user)) {
96                     return vol;
97                 }
98             }
99         }
100 
101         throw new FileNotFoundException("Couldn't find volume with name " + volumeName);
102     }
103 
getVolumePath(@onNull String volumeName, @NonNull UserHandle user)104     public @NonNull File getVolumePath(@NonNull String volumeName, @NonNull UserHandle user)
105             throws FileNotFoundException {
106         synchronized (mLock) {
107             try {
108                 MediaVolume volume = findVolume(volumeName, user);
109                 return volume.getPath();
110             } catch (FileNotFoundException e) {
111                 Log.w(TAG, "getVolumePath for unknown volume: " + volumeName);
112                 // Try again by using FileUtils below
113             }
114 
115             final Context userContext = mUserCache.getContextForUser(user);
116             return FileUtils.getVolumePath(userContext, volumeName);
117         }
118     }
119 
getVolumeScanPaths(@onNull String volumeName, @NonNull UserHandle user)120     public @NonNull Collection<File> getVolumeScanPaths(@NonNull String volumeName,
121             @NonNull UserHandle user) throws FileNotFoundException {
122         synchronized (mLock) {
123             if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
124                 // Internal is shared by all users
125                 return mCachedInternalScanPaths;
126             }
127             try {
128                 MediaVolume volume = findVolume(volumeName, user);
129                 if (mCachedVolumeScanPaths.containsKey(volume)) {
130                     return mCachedVolumeScanPaths.get(volume);
131                 }
132             } catch (FileNotFoundException e) {
133                 Log.w(TAG, "Didn't find cached volume scan paths for " + volumeName);
134             }
135 
136             // Nothing found above; let's ask directly
137             final Context userContext = mUserCache.getContextForUser(user);
138             final Collection<File> res = FileUtils.getVolumeScanPaths(userContext, volumeName);
139 
140             return res;
141         }
142     }
143 
findVolumeForFile(@onNull File file)144     public @NonNull MediaVolume findVolumeForFile(@NonNull File file) throws FileNotFoundException {
145         synchronized (mLock) {
146             for (MediaVolume volume : mExternalVolumes) {
147                 if (FileUtils.contains(volume.getPath(), file)) {
148                     return volume;
149                 }
150             }
151         }
152 
153         Log.w(TAG, "Didn't find any volume for getVolume(" + file.getPath() + ")");
154         // Nothing found above; let's ask directly
155         final StorageManager sm = mContext.getSystemService(StorageManager.class);
156         final StorageVolume volume = sm.getStorageVolume(file);
157         if (volume == null) {
158             throw new FileNotFoundException("Missing volume for " + file);
159         }
160 
161         return MediaVolume.fromStorageVolume(volume);
162     }
163 
getVolumeId(@onNull File file)164     public @NonNull String getVolumeId(@NonNull File file) throws FileNotFoundException {
165         MediaVolume volume = findVolumeForFile(file);
166 
167         return volume.getId();
168     }
169 
170     @GuardedBy("mLock")
updateExternalVolumesForUserLocked(Context userContext)171     private void updateExternalVolumesForUserLocked(Context userContext) {
172         final StorageManager sm = userContext.getSystemService(StorageManager.class);
173         for (String volumeName : MediaStore.getExternalVolumeNames(userContext)) {
174             try {
175                 final Uri uri = MediaStore.Files.getContentUri(volumeName);
176                 final StorageVolume storageVolume = sm.getStorageVolume(uri);
177                 MediaVolume volume = MediaVolume.fromStorageVolume(storageVolume);
178                 mExternalVolumes.add(volume);
179                 mCachedVolumeScanPaths.put(volume, FileUtils.getVolumeScanPaths(userContext,
180                             volume.getName()));
181             } catch (IllegalStateException | FileNotFoundException e) {
182                 Log.wtf(TAG, "Failed to update volume " + volumeName, e);
183             }
184         }
185     }
186 
update()187     public void update() {
188         synchronized (mLock) {
189             mCachedVolumeScanPaths.clear();
190             try {
191                 mCachedInternalScanPaths = FileUtils.getVolumeScanPaths(mContext,
192                         MediaStore.VOLUME_INTERNAL);
193             } catch (FileNotFoundException e) {
194                 Log.wtf(TAG, "Failed to update volume " + MediaStore.VOLUME_INTERNAL,e );
195             }
196             mExternalVolumes.clear();
197             List<UserHandle> users = mUserCache.updateAndGetUsers();
198             for (UserHandle user : users) {
199                 Context userContext = mUserCache.getContextForUser(user);
200                 updateExternalVolumesForUserLocked(userContext);
201             }
202         }
203     }
204 
dump(PrintWriter writer)205     public void dump(PrintWriter writer) {
206         writer.println("Volume cache state:");
207         synchronized (mLock) {
208             for (MediaVolume volume : mExternalVolumes)  {
209                 writer.println("  " + volume.toString());
210             }
211         }
212     }
213 }
214