blob: 0c70e104f9a612d9d3192f4b8f50cd50022673b9 [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
Ivan Chiang730b3a32019-08-21 16:12:54 +0800278 /**
279 * Check that the directory is the root of storage or blocked file from tree.
280 *
281 * @param docId the docId of the directory to be checked
282 * @return true, should be blocked from tree. Otherwise, false.
283 */
284 @Override
285 protected boolean shouldBlockFromTree(@NonNull String docId) {
286 try {
Ivan Chiang698340f2019-11-15 19:02:46 +0800287 final File dir = getFileForDocId(docId, false /* visible */);
288
289 // the file is null or it is not a directory
290 if (dir == null || !dir.isDirectory()) {
Ivan Chiang730b3a32019-08-21 16:12:54 +0800291 return false;
292 }
293
Stephen Hughes4827cdf2020-01-15 13:03:56 -0800294 // Allow all directories on USB, including the root.
295 try {
296 RootInfo rootInfo = getRootFromDocId(docId);
297 if ((rootInfo.flags & Root.FLAG_REMOVABLE_USB) == Root.FLAG_REMOVABLE_USB) {
298 return false;
299 }
300 } catch (FileNotFoundException e) {
301 Log.e(TAG, "Failed to determine rootInfo for docId");
302 }
303
Ivan Chiang698340f2019-11-15 19:02:46 +0800304 final String path = getPathFromDocId(docId);
Ivan Chiang730b3a32019-08-21 16:12:54 +0800305
Ivan Chiang698340f2019-11-15 19:02:46 +0800306 // Block the root of the storage
307 if (path.isEmpty()) {
Ivan Chiang730b3a32019-08-21 16:12:54 +0800308 return true;
309 }
310
Ivan Chiang698340f2019-11-15 19:02:46 +0800311 // Block Download folder from tree
312 if (TextUtils.equals(Environment.DIRECTORY_DOWNLOADS.toLowerCase(),
313 path.toLowerCase())) {
314 return true;
Ivan Chiang730b3a32019-08-21 16:12:54 +0800315 }
316
Ivan Chiang730b3a32019-08-21 16:12:54 +0800317 return false;
318 } catch (IOException e) {
319 throw new IllegalArgumentException(
320 "Failed to determine if " + docId + " should block from tree " + ": " + e);
321 }
322 }
323
Garfield Tan75379db2017-02-08 15:32:56 -0800324 @Override
325 protected String getDocIdForFile(File file) throws FileNotFoundException {
Felipe Lemeb012f912016-01-22 16:49:55 -0800326 return getDocIdForFileMaybeCreate(file, false);
327 }
328
329 private String getDocIdForFileMaybeCreate(File file, boolean createNewDir)
330 throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700331 String path = file.getAbsolutePath();
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700332
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700333 // Find the most-specific root path
Garfield Tandc9593e2017-01-09 18:04:43 -0800334 boolean visiblePath = false;
335 RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false);
336
337 if (mostSpecificRoot == null) {
338 // Try visible path if no internal path matches. MediaStore uses visible paths.
339 visiblePath = true;
340 mostSpecificRoot = getMostSpecificRootForPath(path, true);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700341 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700342
Garfield Tandc9593e2017-01-09 18:04:43 -0800343 if (mostSpecificRoot == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700344 throw new FileNotFoundException("Failed to find root that contains " + path);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700345 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700346
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700347 // Start at first char of path under root
Garfield Tandc9593e2017-01-09 18:04:43 -0800348 final String rootPath = visiblePath
349 ? mostSpecificRoot.visiblePath.getAbsolutePath()
350 : mostSpecificRoot.path.getAbsolutePath();
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700351 if (rootPath.equals(path)) {
352 path = "";
353 } else if (rootPath.endsWith("/")) {
354 path = path.substring(rootPath.length());
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700355 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700356 path = path.substring(rootPath.length() + 1);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700357 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700358
Felipe Lemeb012f912016-01-22 16:49:55 -0800359 if (!file.exists() && createNewDir) {
360 Log.i(TAG, "Creating new directory " + file);
361 if (!file.mkdir()) {
362 Log.e(TAG, "Could not create directory " + file);
363 }
364 }
365
Garfield Tandc9593e2017-01-09 18:04:43 -0800366 return mostSpecificRoot.rootId + ':' + path;
367 }
368
369 private RootInfo getMostSpecificRootForPath(String path, boolean visible) {
370 // Find the most-specific root path
371 RootInfo mostSpecificRoot = null;
372 String mostSpecificPath = null;
373 synchronized (mRootsLock) {
374 for (int i = 0; i < mRoots.size(); i++) {
375 final RootInfo root = mRoots.valueAt(i);
376 final File rootFile = visible ? root.visiblePath : root.path;
377 if (rootFile != null) {
378 final String rootPath = rootFile.getAbsolutePath();
379 if (path.startsWith(rootPath) && (mostSpecificPath == null
380 || rootPath.length() > mostSpecificPath.length())) {
381 mostSpecificRoot = root;
382 mostSpecificPath = rootPath;
383 }
384 }
385 }
386 }
387
388 return mostSpecificRoot;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700389 }
390
Garfield Tan75379db2017-02-08 15:32:56 -0800391 @Override
392 protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600393 return getFileForDocId(docId, visible, true);
394 }
395
396 private File getFileForDocId(String docId, boolean visible, boolean mustExist)
397 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700398 RootInfo root = getRootFromDocId(docId);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600399 return buildFile(root, docId, visible, mustExist);
Garfield Tanaba97f32016-10-06 17:34:19 +0000400 }
401
402 private Pair<RootInfo, File> resolveDocId(String docId, boolean visible)
403 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700404 RootInfo root = getRootFromDocId(docId);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600405 return Pair.create(root, buildFile(root, docId, visible, true));
Garfield Tan06940e12016-10-07 16:03:17 -0700406 }
407
Ivan Chiang698340f2019-11-15 19:02:46 +0800408 @VisibleForTesting
409 static String getPathFromDocId(String docId) {
410 final int splitIndex = docId.indexOf(':', 1);
411 final String path = docId.substring(splitIndex + 1);
412
413 if (path.isEmpty()) {
414 return path;
415 }
416
417 // remove trailing "/"
418 if (path.charAt(path.length() - 1) == '/') {
419 return path.substring(0, path.length() - 1);
420 } else {
421 return path;
422 }
423 }
424
Garfield Tan06940e12016-10-07 16:03:17 -0700425 private RootInfo getRootFromDocId(String docId) throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700426 final int splitIndex = docId.indexOf(':', 1);
427 final String tag = docId.substring(0, splitIndex);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700428
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700429 RootInfo root;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700430 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700431 root = mRoots.get(tag);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700432 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700433 if (root == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700434 throw new FileNotFoundException("No root for " + tag);
435 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700436
Garfield Tan06940e12016-10-07 16:03:17 -0700437 return root;
438 }
439
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600440 private File buildFile(RootInfo root, String docId, boolean visible, boolean mustExist)
Garfield Tan06940e12016-10-07 16:03:17 -0700441 throws FileNotFoundException {
442 final int splitIndex = docId.indexOf(':', 1);
443 final String path = docId.substring(splitIndex + 1);
444
Diksha Gohlyan0d1313a2020-04-23 08:02:13 -0700445 File target = root.visiblePath != null ? root.visiblePath : root.path;
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700446 if (target == null) {
447 return null;
448 }
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700449 if (!target.exists()) {
450 target.mkdirs();
451 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700452 target = new File(target, path);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600453 if (mustExist && !target.exists()) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700454 throw new FileNotFoundException("Missing file for " + docId + " at " + target);
455 }
Garfield Tan06940e12016-10-07 16:03:17 -0700456 return target;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700457 }
458
Garfield Tan75379db2017-02-08 15:32:56 -0800459 @Override
460 protected Uri buildNotificationUri(String docId) {
461 return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700462 }
463
464 @Override
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600465 protected void onDocIdChanged(String docId) {
466 try {
467 // Touch the visible path to ensure that any sdcardfs caches have
468 // been updated to reflect underlying changes on disk.
469 final File visiblePath = getFileForDocId(docId, true, false);
470 if (visiblePath != null) {
471 Os.access(visiblePath.getAbsolutePath(), OsConstants.F_OK);
472 }
473 } catch (FileNotFoundException | ErrnoException ignored) {
474 }
475 }
476
477 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700478 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
479 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700480 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700481 for (RootInfo root : mRoots.values()) {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700482 final RowBuilder row = result.newRow();
483 row.add(Root.COLUMN_ROOT_ID, root.rootId);
484 row.add(Root.COLUMN_FLAGS, root.flags);
485 row.add(Root.COLUMN_TITLE, root.title);
486 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
Ivan Chiangc26d3c22019-01-10 19:29:01 +0800487 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600488
489 long availableBytes = -1;
490 if (root.reportAvailableBytes) {
491 if (root.storageUuid != null) {
492 try {
493 availableBytes = getContext()
494 .getSystemService(StorageStatsManager.class)
495 .getFreeBytes(root.storageUuid);
496 } catch (IOException e) {
497 Log.w(TAG, e);
498 }
499 } else {
500 availableBytes = root.path.getUsableSpace();
501 }
502 }
503 row.add(Root.COLUMN_AVAILABLE_BYTES, availableBytes);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700504 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700505 }
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700506 return result;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700507 }
508
509 @Override
Garfield Tanb690b4d2017-03-01 16:05:23 -0800510 public Path findDocumentPath(@Nullable String parentDocId, String childDocId)
Garfield Tanaba97f32016-10-06 17:34:19 +0000511 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700512 final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false);
513 final RootInfo root = resolvedDocId.first;
514 File child = resolvedDocId.second;
Garfield Tanaba97f32016-10-06 17:34:19 +0000515
Garfield Tan06940e12016-10-07 16:03:17 -0700516 final File parent = TextUtils.isEmpty(parentDocId)
517 ? root.path
518 : getFileForDocId(parentDocId);
519
Garfield Tan75379db2017-02-08 15:32:56 -0800520 return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child));
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700521 }
522
Garfield Tan92b96ba2016-11-01 14:33:48 -0700523 private Uri getDocumentUri(String path, List<UriPermission> accessUriPermissions)
524 throws FileNotFoundException {
525 File doc = new File(path);
526
527 final String docId = getDocIdForFile(doc);
528
529 UriPermission docUriPermission = null;
530 UriPermission treeUriPermission = null;
531 for (UriPermission uriPermission : accessUriPermissions) {
532 final Uri uri = uriPermission.getUri();
533 if (AUTHORITY.equals(uri.getAuthority())) {
534 boolean matchesRequestedDoc = false;
535 if (DocumentsContract.isTreeUri(uri)) {
536 final String parentDocId = DocumentsContract.getTreeDocumentId(uri);
Garfield Tandc9593e2017-01-09 18:04:43 -0800537 if (isChildDocument(parentDocId, docId)) {
Garfield Tan92b96ba2016-11-01 14:33:48 -0700538 treeUriPermission = uriPermission;
539 matchesRequestedDoc = true;
540 }
541 } else {
542 final String candidateDocId = DocumentsContract.getDocumentId(uri);
Garfield Tandc9593e2017-01-09 18:04:43 -0800543 if (Objects.equals(docId, candidateDocId)) {
Garfield Tan92b96ba2016-11-01 14:33:48 -0700544 docUriPermission = uriPermission;
545 matchesRequestedDoc = true;
546 }
547 }
548
549 if (matchesRequestedDoc && allowsBothReadAndWrite(uriPermission)) {
550 // This URI permission provides everything an app can get, no need to
551 // further check any other granted URI.
552 break;
553 }
554 }
555 }
556
557 // Full permission URI first.
558 if (allowsBothReadAndWrite(treeUriPermission)) {
559 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
560 }
561
562 if (allowsBothReadAndWrite(docUriPermission)) {
563 return docUriPermission.getUri();
564 }
565
566 // Then partial permission URI.
567 if (treeUriPermission != null) {
568 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
569 }
570
571 if (docUriPermission != null) {
572 return docUriPermission.getUri();
573 }
574
575 throw new SecurityException("The app is not given any access to the document under path " +
576 path + " with permissions granted in " + accessUriPermissions);
577 }
578
579 private static boolean allowsBothReadAndWrite(UriPermission permission) {
580 return permission != null
581 && permission.isReadPermission()
582 && permission.isWritePermission();
583 }
584
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700585 @Override
Ivan Chianga972d042018-10-15 15:23:02 +0800586 public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700587 throws FileNotFoundException {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700588 final File parent;
Diksha Gohlyanf555b7b2020-04-29 10:06:03 -0700589
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700590 synchronized (mRootsLock) {
Diksha Gohlyanf555b7b2020-04-29 10:06:03 -0700591 RootInfo root = mRoots.get(rootId);
592 parent = root.visiblePath != null ? root.visiblePath
593 : root.path;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700594 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700595
Ivan Chianga972d042018-10-15 15:23:02 +0800596 return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700597 }
598
599 @Override
Garfield Tan87877032017-03-22 12:01:14 -0700600 public void ejectRoot(String rootId) {
Ben Line7822fb2016-06-24 15:21:08 -0700601 final long token = Binder.clearCallingIdentity();
Ben Line7822fb2016-06-24 15:21:08 -0700602 RootInfo root = mRoots.get(rootId);
603 if (root != null) {
604 try {
605 mStorageManager.unmount(root.volumeId);
Ben Line7822fb2016-06-24 15:21:08 -0700606 } catch (RuntimeException e) {
Garfield Tan87877032017-03-22 12:01:14 -0700607 throw new IllegalStateException(e);
Ben Line7822fb2016-06-24 15:21:08 -0700608 } finally {
609 Binder.restoreCallingIdentity(token);
610 }
611 }
Ben Line7822fb2016-06-24 15:21:08 -0700612 }
613
614 @Override
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700615 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
616 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160);
617 synchronized (mRootsLock) {
618 for (int i = 0; i < mRoots.size(); i++) {
619 final RootInfo root = mRoots.valueAt(i);
620 pw.println("Root{" + root.rootId + "}:");
621 pw.increaseIndent();
622 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
623 pw.println();
624 pw.printPair("title", root.title);
625 pw.printPair("docId", root.docId);
626 pw.println();
627 pw.printPair("path", root.path);
628 pw.printPair("visiblePath", root.visiblePath);
629 pw.decreaseIndent();
630 pw.println();
631 }
632 }
633 }
634
Felipe Lemeb012f912016-01-22 16:49:55 -0800635 @Override
636 public Bundle call(String method, String arg, Bundle extras) {
637 Bundle bundle = super.call(method, arg, extras);
638 if (bundle == null && !TextUtils.isEmpty(method)) {
639 switch (method) {
640 case "getDocIdForFileCreateNewDir": {
641 getContext().enforceCallingPermission(
642 android.Manifest.permission.MANAGE_DOCUMENTS, null);
643 if (TextUtils.isEmpty(arg)) {
644 return null;
645 }
646 try {
647 final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
648 bundle = new Bundle();
649 bundle.putString("DOC_ID", docId);
650 } catch (FileNotFoundException e) {
651 Log.w(TAG, "file '" + arg + "' not found");
652 return null;
653 }
654 break;
655 }
Jeff Sharkey40182832019-12-18 14:06:48 -0700656 case GET_DOCUMENT_URI_CALL: {
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600657 // All callers must go through MediaProvider
658 getContext().enforceCallingPermission(
659 android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
Garfield Tan92b96ba2016-11-01 14:33:48 -0700660
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600661 final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
662 final List<UriPermission> accessUriPermissions = extras
663 .getParcelableArrayList(DocumentsContract.EXTRA_URI_PERMISSIONS);
664
665 final String path = fileUri.getPath();
Garfield Tan92b96ba2016-11-01 14:33:48 -0700666 try {
667 final Bundle out = new Bundle();
668 final Uri uri = getDocumentUri(path, accessUriPermissions);
669 out.putParcelable(DocumentsContract.EXTRA_URI, uri);
670 return out;
671 } catch (FileNotFoundException e) {
672 throw new IllegalStateException("File in " + path + " is not found.", e);
673 }
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600674 }
Jeff Sharkey40182832019-12-18 14:06:48 -0700675 case GET_MEDIA_URI_CALL: {
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600676 // All callers must go through MediaProvider
677 getContext().enforceCallingPermission(
678 android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
Garfield Tan92b96ba2016-11-01 14:33:48 -0700679
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600680 final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
681 final String docId = DocumentsContract.getDocumentId(documentUri);
682 try {
683 final Bundle out = new Bundle();
Jeff Sharkey601c3c62019-05-29 12:55:05 -0600684 final Uri uri = Uri.fromFile(getFileForDocId(docId, true));
Jeff Sharkey643e99e2018-10-22 18:01:27 -0600685 out.putParcelable(DocumentsContract.EXTRA_URI, uri);
686 return out;
687 } catch (FileNotFoundException e) {
688 throw new IllegalStateException(e);
689 }
Garfield Tan92b96ba2016-11-01 14:33:48 -0700690 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800691 default:
692 Log.w(TAG, "unknown method passed to call(): " + method);
693 }
694 }
695 return bundle;
696 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700697}