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