blob: 8f919c3d86cae4664f341cdd65ec98b5d9bb3376 [file] [log] [blame]
Jeff Sharkey9e0036e2013-04-26 16:54:55 -07001/*
2 * Copyright (C) 2013 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
17package com.android.externalstorage;
18
Ivan Chiang730b3a32019-08-21 16:12:54 +080019import android.annotation.NonNull;
Garfield Tan06940e12016-10-07 16:03:17 -070020import android.annotation.Nullable;
Jeff Sharkey06823d42017-05-09 16:55:29 -060021import android.app.usage.StorageStatsManager;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070022import android.content.ContentResolver;
Garfield Tan92b96ba2016-11-01 14:33:48 -070023import android.content.UriPermission;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070024import android.database.Cursor;
25import android.database.MatrixCursor;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070026import android.database.MatrixCursor.RowBuilder;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070027import android.net.Uri;
Ben Line7822fb2016-06-24 15:21:08 -070028import android.os.Binder;
Felipe Lemeb012f912016-01-22 16:49:55 -080029import android.os.Bundle;
Steve McKay5c462a02016-01-29 16:13:21 -080030import android.os.Environment;
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -070031import android.os.IBinder;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070032import android.os.UserHandle;
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -070033import android.os.UserManager;
Steve McKayba23e542016-03-02 15:15:00 -080034import android.os.storage.DiskInfo;
Zim5631b6e2020-05-01 15:54:55 +010035import android.os.storage.StorageEventListener;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070036import android.os.storage.StorageManager;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070037import android.os.storage.VolumeInfo;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070038import android.provider.DocumentsContract;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070039import android.provider.DocumentsContract.Document;
Garfield Tanaba97f32016-10-06 17:34:19 +000040import android.provider.DocumentsContract.Path;
Garfield Tan06940e12016-10-07 16:03:17 -070041import android.provider.DocumentsContract.Root;
Steve McKayecec7cb2016-03-02 11:35:39 -080042import android.provider.Settings;
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -060043import android.system.ErrnoException;
44import android.system.Os;
45import android.system.OsConstants;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -070046import android.text.TextUtils;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070047import android.util.ArrayMap;
48import android.util.DebugUtils;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070049import android.util.Log;
Garfield Tanaba97f32016-10-06 17:34:19 +000050import android.util.Pair;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070051
Jeff Sharkey1f706c62013-10-17 10:52:17 -070052import com.android.internal.annotations.GuardedBy;
Ivan Chiang698340f2019-11-15 19:02:46 +080053import com.android.internal.annotations.VisibleForTesting;
Garfield Tan75379db2017-02-08 15:32:56 -080054import com.android.internal.content.FileSystemProvider;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070055import com.android.internal.util.IndentingPrintWriter;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070056
57import java.io.File;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070058import java.io.FileDescriptor;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070059import java.io.FileNotFoundException;
Jeff Sharkey06823d42017-05-09 16:55:29 -060060import java.io.IOException;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070061import java.io.PrintWriter;
Garfield Tan75379db2017-02-08 15:32:56 -080062import java.util.Collections;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070063import java.util.List;
Garfield Tan92b96ba2016-11-01 14:33:48 -070064import java.util.Objects;
Jeff Sharkey06823d42017-05-09 16:55:29 -060065import java.util.UUID;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070066
Garfield Tan75379db2017-02-08 15:32:56 -080067public class ExternalStorageProvider extends FileSystemProvider {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070068 private static final String TAG = "ExternalStorage";
69
Steve McKayecec7cb2016-03-02 11:35:39 -080070 private static final boolean DEBUG = false;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070071
Amin Shaikh305e87e2018-11-19 12:33:27 -050072 public static final String AUTHORITY = DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070073
Makoto Onuki14a6df72015-07-01 14:55:14 -070074 private static final Uri BASE_URI =
75 new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
76
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070077 // docId format: root:path/to/file
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070078
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070079 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Jeff Sharkey6efba222013-09-27 16:44:11 -070080 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
Ivan Chiangc26d3c22019-01-10 19:29:01 +080081 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_QUERY_ARGS
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070082 };
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070083
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070084 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
85 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
86 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
87 };
88
89 private static class RootInfo {
90 public String rootId;
Ben Line7822fb2016-06-24 15:21:08 -070091 public String volumeId;
Jeff Sharkey06823d42017-05-09 16:55:29 -060092 public UUID storageUuid;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070093 public int flags;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070094 public String title;
95 public String docId;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070096 public File visiblePath;
97 public File path;
Steve McKayc6a4cd82015-11-18 14:56:50 -080098 public boolean reportAvailableBytes = true;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070099 }
100
Amin Shaikh305e87e2018-11-19 12:33:27 -0500101 private static final String ROOT_ID_PRIMARY_EMULATED =
102 DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700103
Jeff Sharkey40182832019-12-18 14:06:48 -0700104 private static final String GET_DOCUMENT_URI_CALL = "get_document_uri";
105 private static final String GET_MEDIA_URI_CALL = "get_media_uri";
106
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700107 private StorageManager mStorageManager;
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700108 private UserManager mUserManager;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700109
110 private final Object mRootsLock = new Object();
111
112 @GuardedBy("mRootsLock")
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700113 private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700114
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700115 @Override
116 public boolean onCreate() {
Garfield Tan75379db2017-02-08 15:32:56 -0800117 super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
118
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700119 mStorageManager = getContext().getSystemService(StorageManager.class);
120 mUserManager = getContext().getSystemService(UserManager.class);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700121
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700122 updateVolumes();
Zim5631b6e2020-05-01 15:54:55 +0100123
124 mStorageManager.registerListener(new StorageEventListener() {
125 @Override
126 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
127 updateVolumes();
128 }
129 });
130
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700131 return true;
132 }
133
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700134 private void enforceShellRestrictions() {
135 if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
136 && mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
137 throw new SecurityException(
138 "Shell user cannot access files for user " + UserHandle.myUserId());
139 }
140 }
141
142 @Override
Philip P. Moltmann128b7032019-09-27 08:44:12 -0700143 protected int enforceReadPermissionInner(Uri uri, String callingPkg,
144 @Nullable String featureId, IBinder callerToken) throws SecurityException {
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700145 enforceShellRestrictions();
Philip P. Moltmann128b7032019-09-27 08:44:12 -0700146 return super.enforceReadPermissionInner(uri, callingPkg, featureId, callerToken);
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700147 }
148
149 @Override
Philip P. Moltmann128b7032019-09-27 08:44:12 -0700150 protected int enforceWritePermissionInner(Uri uri, String callingPkg,
151 @Nullable String featureId, IBinder callerToken) throws SecurityException {
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700152 enforceShellRestrictions();
Philip P. Moltmann128b7032019-09-27 08:44:12 -0700153 return super.enforceWritePermissionInner(uri, callingPkg, featureId, callerToken);
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700154 }
155
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700156 public void updateVolumes() {
157 synchronized (mRootsLock) {
158 updateVolumesLocked();
159 }
160 }
161
Andreas Gampeb58893072018-09-05 16:52:31 -0700162 @GuardedBy("mRootsLock")
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700163 private void updateVolumesLocked() {
164 mRoots.clear();
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700165
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700166 final int userId = UserHandle.myUserId();
167 final List<VolumeInfo> volumes = mStorageManager.getVolumes();
168 for (VolumeInfo volume : volumes) {
Zim2dca3202019-11-26 11:36:04 +0000169 if (!volume.isMountedReadable() || volume.getMountUserId() != userId) continue;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700170
171 final String rootId;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700172 final String title;
Jeff Sharkey06823d42017-05-09 16:55:29 -0600173 final UUID storageUuid;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700174 if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
Zim17be6f92019-09-25 14:37:55 +0100175 // We currently only support a single emulated volume per user mounted at
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700176 // a time, and it's always considered the primary
Steve McKayecec7cb2016-03-02 11:35:39 -0800177 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700178 rootId = ROOT_ID_PRIMARY_EMULATED;
Steve McKayecec7cb2016-03-02 11:35:39 -0800179
Zim17be6f92019-09-25 14:37:55 +0100180 if (volume.isPrimaryEmulatedForUser(userId)) {
Steve McKayecec7cb2016-03-02 11:35:39 -0800181 // This is basically the user's primary device storage.
182 // Use device name for the volume since this is likely same thing
183 // the user sees when they mount their phone on another device.
184 String deviceName = Settings.Global.getString(
185 getContext().getContentResolver(), Settings.Global.DEVICE_NAME);
186
187 // Device name should always be set. In case it isn't, though,
188 // fall back to a localized "Internal Storage" string.
189 title = !TextUtils.isEmpty(deviceName)
190 ? deviceName
191 : getContext().getString(R.string.root_internal_storage);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600192 storageUuid = StorageManager.UUID_DEFAULT;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700193 } else {
Steve McKayecec7cb2016-03-02 11:35:39 -0800194 // This should cover all other storage devices, like an SD card
195 // or USB OTG drive plugged in. Using getBestVolumeDescription()
196 // will give us a nice string like "Samsung SD card" or "SanDisk USB drive"
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700197 final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
198 title = mStorageManager.getBestVolumeDescription(privateVol);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600199 storageUuid = StorageManager.convert(privateVol.fsUuid);
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700200 }
Zim2dca3202019-11-26 11:36:04 +0000201 } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC
202 || volume.getType() == VolumeInfo.TYPE_STUB) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700203 rootId = volume.getFsUuid();
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700204 title = mStorageManager.getBestVolumeDescription(volume);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600205 storageUuid = null;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700206 } else {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700207 // Unsupported volume; ignore
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700208 continue;
209 }
210
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700211 if (TextUtils.isEmpty(rootId)) {
212 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
213 continue;
214 }
215 if (mRoots.containsKey(rootId)) {
216 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700217 continue;
218 }
219
Steve McKayc6a4cd82015-11-18 14:56:50 -0800220 final RootInfo root = new RootInfo();
221 mRoots.put(rootId, root);
222
223 root.rootId = rootId;
Ben Line7822fb2016-06-24 15:21:08 -0700224 root.volumeId = volume.id;
Jeff Sharkey06823d42017-05-09 16:55:29 -0600225 root.storageUuid = storageUuid;
Steve McKayefa17612016-01-29 18:15:39 -0800226 root.flags = Root.FLAG_LOCAL_ONLY
Garfield Tanaba97f32016-10-06 17:34:19 +0000227 | Root.FLAG_SUPPORTS_SEARCH
228 | Root.FLAG_SUPPORTS_IS_CHILD;
Steve McKayc6a4cd82015-11-18 14:56:50 -0800229
Steve McKayba23e542016-03-02 15:15:00 -0800230 final DiskInfo disk = volume.getDisk();
231 if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk);
232 if (disk != null && disk.isSd()) {
233 root.flags |= Root.FLAG_REMOVABLE_SD;
234 } else if (disk != null && disk.isUsb()) {
235 root.flags |= Root.FLAG_REMOVABLE_USB;
236 }
237
Takamasa Kuramitsuf5f03fc2018-10-04 17:56:37 +0900238 if (volume.getType() != VolumeInfo.TYPE_EMULATED) {
Ben Line7822fb2016-06-24 15:21:08 -0700239 root.flags |= Root.FLAG_SUPPORTS_EJECT;
240 }
241
Steve McKayefa17612016-01-29 18:15:39 -0800242 if (volume.isPrimary()) {
Aga Wronska1719b352016-03-21 11:28:03 -0700243 root.flags |= Root.FLAG_ADVANCED;
Steve McKayefa17612016-01-29 18:15:39 -0800244 }
Steve McKayc6a4cd82015-11-18 14:56:50 -0800245 // Dunno when this would NOT be the case, but never hurts to be correct.
246 if (volume.isMountedWritable()) {
247 root.flags |= Root.FLAG_SUPPORTS_CREATE;
248 }
249 root.title = title;
250 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
251 root.flags |= Root.FLAG_HAS_SETTINGS;
252 }
253 if (volume.isVisibleForRead(userId)) {
254 root.visiblePath = volume.getPathForUser(userId);
255 } else {
256 root.visiblePath = null;
257 }
258 root.path = volume.getInternalPathForUser(userId);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700259 try {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700260 root.docId = getDocIdForFile(root.path);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800261 } catch (FileNotFoundException e) {
262 throw new IllegalStateException(e);
263 }
264 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700265
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700266 Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
267
Makoto Onuki14a6df72015-07-01 14:55:14 -0700268 // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
269 // as well as content://com.android.externalstorage.documents/document/*/children,
270 // so just notify on content://com.android.externalstorage.documents/.
271 getContext().getContentResolver().notifyChange(BASE_URI, null, false);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700272 }
273
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700274 private static String[] resolveRootProjection(String[] projection) {
275 return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
276 }
277
Abhijeet Kaur553625d2020-05-07 13:11:55 +0100278 @Override
279 public Cursor queryChildDocumentsForManage(
280 String parentDocId, String[] projection, String sortOrder)
281 throws FileNotFoundException {
282 return queryChildDocumentsShowAll(parentDocId, projection, sortOrder);
283 }
284
Ivan Chiang730b3a32019-08-21 16:12:54 +0800285 /**
286 * Check that the directory is the root of storage or blocked file from tree.
287 *
288 * @param docId the docId of the directory to be checked
289 * @return true, should be blocked from tree. Otherwise, false.
290 */
291 @Override
292 protected boolean shouldBlockFromTree(@NonNull String docId) {
293 try {
Ivan Chiang698340f2019-11-15 19:02:46 +0800294 final File dir = getFileForDocId(docId, false /* visible */);
295
296 // the file is null or it is not a directory
297 if (dir == null || !dir.isDirectory()) {
Ivan Chiang730b3a32019-08-21 16:12:54 +0800298 return false;
299 }
300
Stephen Hughes4827cdf2020-01-15 13:03:56 -0800301 // Allow all directories on USB, including the root.
302 try {
303 RootInfo rootInfo = getRootFromDocId(docId);
304 if ((rootInfo.flags & Root.FLAG_REMOVABLE_USB) == Root.FLAG_REMOVABLE_USB) {
305 return false;
306 }
307 } catch (FileNotFoundException e) {
308 Log.e(TAG, "Failed to determine rootInfo for docId");
309 }
310
Ivan Chiang698340f2019-11-15 19:02:46 +0800311 final String path = getPathFromDocId(docId);
Ivan Chiang730b3a32019-08-21 16:12:54 +0800312
Ivan Chiang698340f2019-11-15 19:02:46 +0800313 // Block the root of the storage
314 if (path.isEmpty()) {
Ivan Chiang730b3a32019-08-21 16:12:54 +0800315 return true;
316 }
317
Ivan Chiang698340f2019-11-15 19:02:46 +0800318 // Block Download folder from tree
319 if (TextUtils.equals(Environment.DIRECTORY_DOWNLOADS.toLowerCase(),
320 path.toLowerCase())) {
321 return true;
Ivan Chiang730b3a32019-08-21 16:12:54 +0800322 }
323
Ivan Chiang730b3a32019-08-21 16:12:54 +0800324 return false;
325 } catch (IOException e) {
326 throw new IllegalArgumentException(
327 "Failed to determine if " + docId + " should block from tree " + ": " + e);
328 }
329 }
330
Garfield Tan75379db2017-02-08 15:32:56 -0800331 @Override
332 protected String getDocIdForFile(File file) throws FileNotFoundException {
Felipe Lemeb012f912016-01-22 16:49:55 -0800333 return getDocIdForFileMaybeCreate(file, false);
334 }
335
336 private String getDocIdForFileMaybeCreate(File file, boolean createNewDir)
337 throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700338 String path = file.getAbsolutePath();
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700339
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700340 // Find the most-specific root path
Garfield Tandc9593e2017-01-09 18:04:43 -0800341 boolean visiblePath = false;
342 RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false);
343
344 if (mostSpecificRoot == null) {
345 // Try visible path if no internal path matches. MediaStore uses visible paths.
346 visiblePath = true;
347 mostSpecificRoot = getMostSpecificRootForPath(path, true);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700348 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700349
Garfield Tandc9593e2017-01-09 18:04:43 -0800350 if (mostSpecificRoot == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700351 throw new FileNotFoundException("Failed to find root that contains " + path);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700352 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700353
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700354 // Start at first char of path under root
Garfield Tandc9593e2017-01-09 18:04:43 -0800355 final String rootPath = visiblePath
356 ? mostSpecificRoot.visiblePath.getAbsolutePath()
357 : mostSpecificRoot.path.getAbsolutePath();
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700358 if (rootPath.equals(path)) {
359 path = "";
360 } else if (rootPath.endsWith("/")) {
361 path = path.substring(rootPath.length());
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700362 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700363 path = path.substring(rootPath.length() + 1);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700364 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700365
Felipe Lemeb012f912016-01-22 16:49:55 -0800366 if (!file.exists() && createNewDir) {
367 Log.i(TAG, "Creating new directory " + file);
368 if (!file.mkdir()) {
369 Log.e(TAG, "Could not create directory " + file);
370 }
371 }
372
Garfield Tandc9593e2017-01-09 18:04:43 -0800373 return mostSpecificRoot.rootId + ':' + path;
374 }
375
376 private RootInfo getMostSpecificRootForPath(String path, boolean visible) {
377 // Find the most-specific root path
378 RootInfo mostSpecificRoot = null;
379 String mostSpecificPath = null;
380 synchronized (mRootsLock) {
381 for (int i = 0; i < mRoots.size(); i++) {
382 final RootInfo root = mRoots.valueAt(i);
383 final File rootFile = visible ? root.visiblePath : root.path;
384 if (rootFile != null) {
385 final String rootPath = rootFile.getAbsolutePath();
386 if (path.startsWith(rootPath) && (mostSpecificPath == null
387 || rootPath.length() > mostSpecificPath.length())) {
388 mostSpecificRoot = root;
389 mostSpecificPath = rootPath;
390 }
391 }
392 }
393 }
394
395 return mostSpecificRoot;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700396 }
397
Garfield Tan75379db2017-02-08 15:32:56 -0800398 @Override
399 protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600400 return getFileForDocId(docId, visible, true);
401 }
402
403 private File getFileForDocId(String docId, boolean visible, boolean mustExist)
404 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700405 RootInfo root = getRootFromDocId(docId);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600406 return buildFile(root, docId, visible, mustExist);
Garfield Tanaba97f32016-10-06 17:34:19 +0000407 }
408
409 private Pair<RootInfo, File> resolveDocId(String docId, boolean visible)
410 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700411 RootInfo root = getRootFromDocId(docId);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600412 return Pair.create(root, buildFile(root, docId, visible, true));
Garfield Tan06940e12016-10-07 16:03:17 -0700413 }
414
Ivan Chiang698340f2019-11-15 19:02:46 +0800415 @VisibleForTesting
416 static String getPathFromDocId(String docId) {
417 final int splitIndex = docId.indexOf(':', 1);
418 final String path = docId.substring(splitIndex + 1);
419
420 if (path.isEmpty()) {
421 return path;
422 }
423
424 // remove trailing "/"
425 if (path.charAt(path.length() - 1) == '/') {
426 return path.substring(0, path.length() - 1);
427 } else {
428 return path;
429 }
430 }
431
Garfield Tan06940e12016-10-07 16:03:17 -0700432 private RootInfo getRootFromDocId(String docId) throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700433 final int splitIndex = docId.indexOf(':', 1);
434 final String tag = docId.substring(0, splitIndex);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700435
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700436 RootInfo root;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700437 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700438 root = mRoots.get(tag);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700439 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700440 if (root == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700441 throw new FileNotFoundException("No root for " + tag);
442 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700443
Garfield Tan06940e12016-10-07 16:03:17 -0700444 return root;
445 }
446
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600447 private File buildFile(RootInfo root, String docId, boolean visible, boolean mustExist)
Garfield Tan06940e12016-10-07 16:03:17 -0700448 throws FileNotFoundException {
449 final int splitIndex = docId.indexOf(':', 1);
450 final String path = docId.substring(splitIndex + 1);
451
Diksha Gohlyan0d1313a2020-04-23 08:02:13 -0700452 File target = root.visiblePath != null ? root.visiblePath : root.path;
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700453 if (target == null) {
454 return null;
455 }
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700456 if (!target.exists()) {
457 target.mkdirs();
458 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700459 target = new File(target, path);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600460 if (mustExist && !target.exists()) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700461 throw new FileNotFoundException("Missing file for " + docId + " at " + target);
462 }
Garfield Tan06940e12016-10-07 16:03:17 -0700463 return target;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700464 }
465
Garfield Tan75379db2017-02-08 15:32:56 -0800466 @Override
467 protected Uri buildNotificationUri(String docId) {
468 return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700469 }
470
471 @Override
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600472 protected void onDocIdChanged(String docId) {
473 try {
474 // Touch the visible path to ensure that any sdcardfs caches have
475 // been updated to reflect underlying changes on disk.
476 final File visiblePath = getFileForDocId(docId, true, false);
477 if (visiblePath != null) {
478 Os.access(visiblePath.getAbsolutePath(), OsConstants.F_OK);
479 }
480 } catch (FileNotFoundException | ErrnoException ignored) {
481 }
482 }
483
484 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700485 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
486 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700487 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700488 for (RootInfo root : mRoots.values()) {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700489 final RowBuilder row = result.newRow();
490 row.add(Root.COLUMN_ROOT_ID, root.rootId);
491 row.add(Root.COLUMN_FLAGS, root.flags);
492 row.add(Root.COLUMN_TITLE, root.title);
493 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
Ivan Chiangc26d3c22019-01-10 19:29:01 +0800494 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600495
496 long availableBytes = -1;
497 if (root.reportAvailableBytes) {
498 if (root.storageUuid != null) {
499 try {
500 availableBytes = getContext()
501 .getSystemService(StorageStatsManager.class)
502 .getFreeBytes(root.storageUuid);
503 } catch (IOException e) {
504 Log.w(TAG, e);
505 }
506 } else {
507 availableBytes = root.path.getUsableSpace();
508 }
509 }
510 row.add(Root.COLUMN_AVAILABLE_BYTES, availableBytes);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700511 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700512 }
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700513 return result;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700514 }
515
516 @Override
Garfield Tanb690b4d2017-03-01 16:05:23 -0800517 public Path findDocumentPath(@Nullable String parentDocId, String childDocId)
Garfield Tanaba97f32016-10-06 17:34:19 +0000518 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700519 final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false);
520 final RootInfo root = resolvedDocId.first;
521 File child = resolvedDocId.second;
Garfield Tanaba97f32016-10-06 17:34:19 +0000522
Garfield Tan06940e12016-10-07 16:03:17 -0700523 final File parent = TextUtils.isEmpty(parentDocId)
524 ? root.path
525 : getFileForDocId(parentDocId);
526
Garfield Tan75379db2017-02-08 15:32:56 -0800527 return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child));
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700528 }
529
Garfield Tan92b96ba2016-11-01 14:33:48 -0700530 private Uri getDocumentUri(String path, List<UriPermission> accessUriPermissions)
531 throws FileNotFoundException {
532 File doc = new File(path);
533
534 final String docId = getDocIdForFile(doc);
535
536 UriPermission docUriPermission = null;
537 UriPermission treeUriPermission = null;
538 for (UriPermission uriPermission : accessUriPermissions) {
539 final Uri uri = uriPermission.getUri();
540 if (AUTHORITY.equals(uri.getAuthority())) {
541 boolean matchesRequestedDoc = false;
542 if (DocumentsContract.isTreeUri(uri)) {
543 final String parentDocId = DocumentsContract.getTreeDocumentId(uri);
Garfield Tandc9593e2017-01-09 18:04:43 -0800544 if (isChildDocument(parentDocId, docId)) {
Garfield Tan92b96ba2016-11-01 14:33:48 -0700545 treeUriPermission = uriPermission;
546 matchesRequestedDoc = true;
547 }
548 } else {
549 final String candidateDocId = DocumentsContract.getDocumentId(uri);
Garfield Tandc9593e2017-01-09 18:04:43 -0800550 if (Objects.equals(docId, candidateDocId)) {
Garfield Tan92b96ba2016-11-01 14:33:48 -0700551 docUriPermission = uriPermission;
552 matchesRequestedDoc = true;
553 }
554 }
555
556 if (matchesRequestedDoc && allowsBothReadAndWrite(uriPermission)) {
557 // This URI permission provides everything an app can get, no need to
558 // further check any other granted URI.
559 break;
560 }
561 }
562 }
563
564 // Full permission URI first.
565 if (allowsBothReadAndWrite(treeUriPermission)) {
566 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
567 }
568
569 if (allowsBothReadAndWrite(docUriPermission)) {
570 return docUriPermission.getUri();
571 }
572
573 // Then partial permission URI.
574 if (treeUriPermission != null) {
575 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
576 }
577
578 if (docUriPermission != null) {
579 return docUriPermission.getUri();
580 }
581
582 throw new SecurityException("The app is not given any access to the document under path " +
583 path + " with permissions granted in " + accessUriPermissions);
584 }
585
586 private static boolean allowsBothReadAndWrite(UriPermission permission) {
587 return permission != null
588 && permission.isReadPermission()
589 && permission.isWritePermission();
590 }
591
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700592 @Override
Ivan Chianga972d042018-10-15 15:23:02 +0800593 public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700594 throws FileNotFoundException {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700595 final File parent;
Diksha Gohlyanf555b7b2020-04-29 10:06:03 -0700596
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700597 synchronized (mRootsLock) {
Diksha Gohlyanf555b7b2020-04-29 10:06:03 -0700598 RootInfo root = mRoots.get(rootId);
599 parent = root.visiblePath != null ? root.visiblePath
600 : root.path;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700601 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700602
Ivan Chianga972d042018-10-15 15:23:02 +0800603 return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700604 }
605
606 @Override
Garfield Tan87877032017-03-22 12:01:14 -0700607 public void ejectRoot(String rootId) {
Ben Line7822fb2016-06-24 15:21:08 -0700608 final long token = Binder.clearCallingIdentity();
Ben Line7822fb2016-06-24 15:21:08 -0700609 RootInfo root = mRoots.get(rootId);
610 if (root != null) {
611 try {
612 mStorageManager.unmount(root.volumeId);
Ben Line7822fb2016-06-24 15:21:08 -0700613 } catch (RuntimeException e) {
Garfield Tan87877032017-03-22 12:01:14 -0700614 throw new IllegalStateException(e);
Ben Line7822fb2016-06-24 15:21:08 -0700615 } finally {
616 Binder.restoreCallingIdentity(token);
617 }
618 }
Ben Line7822fb2016-06-24 15:21:08 -0700619 }
620
621 @Override
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700622 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
623 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160);
624 synchronized (mRootsLock) {
625 for (int i = 0; i < mRoots.size(); i++) {
626 final RootInfo root = mRoots.valueAt(i);
627 pw.println("Root{" + root.rootId + "}:");
628 pw.increaseIndent();
629 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
630 pw.println();
631 pw.printPair("title", root.title);
632 pw.printPair("docId", root.docId);
633 pw.println();
634 pw.printPair("path", root.path);
635 pw.printPair("visiblePath", root.visiblePath);
636 pw.decreaseIndent();
637 pw.println();
638 }
639 }
640 }
641
Felipe Lemeb012f912016-01-22 16:49:55 -0800642 @Override
643 public Bundle call(String method, String arg, Bundle extras) {
644 Bundle bundle = super.call(method, arg, extras);
645 if (bundle == null && !TextUtils.isEmpty(method)) {
646 switch (method) {
647 case "getDocIdForFileCreateNewDir": {
648 getContext().enforceCallingPermission(
649 android.Manifest.permission.MANAGE_DOCUMENTS, null);
650 if (TextUtils.isEmpty(arg)) {
651 return null;
652 }
653 try {
654 final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
655 bundle = new Bundle();
656 bundle.putString("DOC_ID", docId);
657 } catch (FileNotFoundException e) {
658 Log.w(TAG, "file '" + arg + "' not found");
659 return null;
660 }
661 break;
662 }
Jeff Sharkey40182832019-12-18 14:06:48 -0700663 case GET_DOCUMENT_URI_CALL: {
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600664 // All callers must go through MediaProvider
665 getContext().enforceCallingPermission(
666 android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
Garfield Tan92b96ba2016-11-01 14:33:48 -0700667
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600668 final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
669 final List<UriPermission> accessUriPermissions = extras
670 .getParcelableArrayList(DocumentsContract.EXTRA_URI_PERMISSIONS);
671
672 final String path = fileUri.getPath();
Garfield Tan92b96ba2016-11-01 14:33:48 -0700673 try {
674 final Bundle out = new Bundle();
675 final Uri uri = getDocumentUri(path, accessUriPermissions);
676 out.putParcelable(DocumentsContract.EXTRA_URI, uri);
677 return out;
678 } catch (FileNotFoundException e) {
679 throw new IllegalStateException("File in " + path + " is not found.", e);
680 }
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600681 }
Jeff Sharkey40182832019-12-18 14:06:48 -0700682 case GET_MEDIA_URI_CALL: {
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600683 // All callers must go through MediaProvider
684 getContext().enforceCallingPermission(
685 android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
Garfield Tan92b96ba2016-11-01 14:33:48 -0700686
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600687 final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
688 final String docId = DocumentsContract.getDocumentId(documentUri);
689 try {
690 final Bundle out = new Bundle();
Jeff Sharkey601c3c62019-05-29 12:55:05 -0600691 final Uri uri = Uri.fromFile(getFileForDocId(docId, true));
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600692 out.putParcelable(DocumentsContract.EXTRA_URI, uri);
693 return out;
694 } catch (FileNotFoundException e) {
695 throw new IllegalStateException(e);
696 }
Garfield Tan92b96ba2016-11-01 14:33:48 -0700697 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800698 default:
699 Log.w(TAG, "unknown method passed to call(): " + method);
700 }
701 }
702 return bundle;
703 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700704}