blob: 8a5d1119696f4a560e5071e49aec2c328e533f00 [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
Garfield Tan06940e12016-10-07 16:03:17 -070019import android.annotation.Nullable;
Jeff Sharkey06823d42017-05-09 16:55:29 -060020import android.app.usage.StorageStatsManager;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070021import android.content.ContentResolver;
Garfield Tan92b96ba2016-11-01 14:33:48 -070022import android.content.UriPermission;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070023import android.database.Cursor;
24import android.database.MatrixCursor;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070025import android.database.MatrixCursor.RowBuilder;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070026import android.net.Uri;
Ben Line7822fb2016-06-24 15:21:08 -070027import android.os.Binder;
Felipe Lemeb012f912016-01-22 16:49:55 -080028import android.os.Bundle;
Steve McKay5c462a02016-01-29 16:13:21 -080029import android.os.Environment;
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -070030import android.os.IBinder;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070031import android.os.UserHandle;
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -070032import android.os.UserManager;
Steve McKayba23e542016-03-02 15:15:00 -080033import android.os.storage.DiskInfo;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070034import android.os.storage.StorageManager;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070035import android.os.storage.VolumeInfo;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070036import android.provider.DocumentsContract;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070037import android.provider.DocumentsContract.Document;
Garfield Tanaba97f32016-10-06 17:34:19 +000038import android.provider.DocumentsContract.Path;
Garfield Tan06940e12016-10-07 16:03:17 -070039import android.provider.DocumentsContract.Root;
Steve McKayecec7cb2016-03-02 11:35:39 -080040import android.provider.Settings;
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -060041import android.system.ErrnoException;
42import android.system.Os;
43import android.system.OsConstants;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -070044import android.text.TextUtils;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070045import android.util.ArrayMap;
46import android.util.DebugUtils;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070047import android.util.Log;
Garfield Tanaba97f32016-10-06 17:34:19 +000048import android.util.Pair;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070049
Jeff Sharkey1f706c62013-10-17 10:52:17 -070050import com.android.internal.annotations.GuardedBy;
Garfield Tan75379db2017-02-08 15:32:56 -080051import com.android.internal.content.FileSystemProvider;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070052import com.android.internal.util.IndentingPrintWriter;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070053
54import java.io.File;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070055import java.io.FileDescriptor;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070056import java.io.FileNotFoundException;
Jeff Sharkey06823d42017-05-09 16:55:29 -060057import java.io.IOException;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070058import java.io.PrintWriter;
Garfield Tan75379db2017-02-08 15:32:56 -080059import java.util.Collections;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070060import java.util.List;
Garfield Tan92b96ba2016-11-01 14:33:48 -070061import java.util.Objects;
Jeff Sharkey06823d42017-05-09 16:55:29 -060062import java.util.UUID;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070063
Garfield Tan75379db2017-02-08 15:32:56 -080064public class ExternalStorageProvider extends FileSystemProvider {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070065 private static final String TAG = "ExternalStorage";
66
Steve McKayecec7cb2016-03-02 11:35:39 -080067 private static final boolean DEBUG = false;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070068
Jeff Sharkey1f706c62013-10-17 10:52:17 -070069 public static final String AUTHORITY = "com.android.externalstorage.documents";
70
Makoto Onuki14a6df72015-07-01 14:55:14 -070071 private static final Uri BASE_URI =
72 new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
73
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070074 // docId format: root:path/to/file
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070075
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070076 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Jeff Sharkey6efba222013-09-27 16:44:11 -070077 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
78 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070079 };
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070080
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070081 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
82 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
83 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
84 };
85
86 private static class RootInfo {
87 public String rootId;
Ben Line7822fb2016-06-24 15:21:08 -070088 public String volumeId;
Jeff Sharkey06823d42017-05-09 16:55:29 -060089 public UUID storageUuid;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070090 public int flags;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070091 public String title;
92 public String docId;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070093 public File visiblePath;
94 public File path;
Steve McKayc6a4cd82015-11-18 14:56:50 -080095 public boolean reportAvailableBytes = true;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070096 }
97
Jeff Sharkey1f706c62013-10-17 10:52:17 -070098 private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
Steve McKayc6a4cd82015-11-18 14:56:50 -080099 private static final String ROOT_ID_HOME = "home";
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700100
101 private StorageManager mStorageManager;
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700102 private UserManager mUserManager;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700103
104 private final Object mRootsLock = new Object();
105
106 @GuardedBy("mRootsLock")
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700107 private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700108
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700109 @Override
110 public boolean onCreate() {
Garfield Tan75379db2017-02-08 15:32:56 -0800111 super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
112
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700113 mStorageManager = getContext().getSystemService(StorageManager.class);
114 mUserManager = getContext().getSystemService(UserManager.class);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700115
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700116 updateVolumes();
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700117 return true;
118 }
119
Jeff Sharkeyb78b754d2018-01-04 15:07:38 -0700120 private void enforceShellRestrictions() {
121 if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
122 && mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
123 throw new SecurityException(
124 "Shell user cannot access files for user " + UserHandle.myUserId());
125 }
126 }
127
128 @Override
129 protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
130 throws SecurityException {
131 enforceShellRestrictions();
132 return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
133 }
134
135 @Override
136 protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
137 throws SecurityException {
138 enforceShellRestrictions();
139 return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
140 }
141
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700142 public void updateVolumes() {
143 synchronized (mRootsLock) {
144 updateVolumesLocked();
145 }
146 }
147
148 private void updateVolumesLocked() {
149 mRoots.clear();
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700150
Steve McKayc6a4cd82015-11-18 14:56:50 -0800151 VolumeInfo primaryVolume = null;
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700152 final int userId = UserHandle.myUserId();
153 final List<VolumeInfo> volumes = mStorageManager.getVolumes();
154 for (VolumeInfo volume : volumes) {
155 if (!volume.isMountedReadable()) continue;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700156
157 final String rootId;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700158 final String title;
Jeff Sharkey06823d42017-05-09 16:55:29 -0600159 final UUID storageUuid;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700160 if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
161 // We currently only support a single emulated volume mounted at
162 // a time, and it's always considered the primary
Steve McKayecec7cb2016-03-02 11:35:39 -0800163 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700164 rootId = ROOT_ID_PRIMARY_EMULATED;
Steve McKayecec7cb2016-03-02 11:35:39 -0800165
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700166 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
Steve McKayecec7cb2016-03-02 11:35:39 -0800167 // This is basically the user's primary device storage.
168 // Use device name for the volume since this is likely same thing
169 // the user sees when they mount their phone on another device.
170 String deviceName = Settings.Global.getString(
171 getContext().getContentResolver(), Settings.Global.DEVICE_NAME);
172
173 // Device name should always be set. In case it isn't, though,
174 // fall back to a localized "Internal Storage" string.
175 title = !TextUtils.isEmpty(deviceName)
176 ? deviceName
177 : getContext().getString(R.string.root_internal_storage);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600178 storageUuid = StorageManager.UUID_DEFAULT;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700179 } else {
Steve McKayecec7cb2016-03-02 11:35:39 -0800180 // This should cover all other storage devices, like an SD card
181 // or USB OTG drive plugged in. Using getBestVolumeDescription()
182 // will give us a nice string like "Samsung SD card" or "SanDisk USB drive"
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700183 final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
184 title = mStorageManager.getBestVolumeDescription(privateVol);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600185 storageUuid = StorageManager.convert(privateVol.fsUuid);
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700186 }
Risan05c41e62018-10-29 08:57:43 +0900187 } else if ((volume.getType() == VolumeInfo.TYPE_PUBLIC
188 || volume.getType() == VolumeInfo.TYPE_STUB)
Jeff Sharkey3b699d72016-10-31 14:33:49 -0600189 && volume.getMountUserId() == userId) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700190 rootId = volume.getFsUuid();
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700191 title = mStorageManager.getBestVolumeDescription(volume);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600192 storageUuid = null;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700193 } else {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700194 // Unsupported volume; ignore
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700195 continue;
196 }
197
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700198 if (TextUtils.isEmpty(rootId)) {
199 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
200 continue;
201 }
202 if (mRoots.containsKey(rootId)) {
203 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700204 continue;
205 }
206
Steve McKayc6a4cd82015-11-18 14:56:50 -0800207 final RootInfo root = new RootInfo();
208 mRoots.put(rootId, root);
209
210 root.rootId = rootId;
Ben Line7822fb2016-06-24 15:21:08 -0700211 root.volumeId = volume.id;
Jeff Sharkey06823d42017-05-09 16:55:29 -0600212 root.storageUuid = storageUuid;
Steve McKayefa17612016-01-29 18:15:39 -0800213 root.flags = Root.FLAG_LOCAL_ONLY
Garfield Tanaba97f32016-10-06 17:34:19 +0000214 | Root.FLAG_SUPPORTS_SEARCH
215 | Root.FLAG_SUPPORTS_IS_CHILD;
Steve McKayc6a4cd82015-11-18 14:56:50 -0800216
Steve McKayba23e542016-03-02 15:15:00 -0800217 final DiskInfo disk = volume.getDisk();
218 if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk);
219 if (disk != null && disk.isSd()) {
220 root.flags |= Root.FLAG_REMOVABLE_SD;
221 } else if (disk != null && disk.isUsb()) {
222 root.flags |= Root.FLAG_REMOVABLE_USB;
223 }
224
Takamasa Kuramitsuf5f03fc2018-10-04 17:56:37 +0900225 if (volume.getType() != VolumeInfo.TYPE_EMULATED) {
Ben Line7822fb2016-06-24 15:21:08 -0700226 root.flags |= Root.FLAG_SUPPORTS_EJECT;
227 }
228
Steve McKayefa17612016-01-29 18:15:39 -0800229 if (volume.isPrimary()) {
230 // save off the primary volume for subsequent "Home" dir initialization.
231 primaryVolume = volume;
Aga Wronska1719b352016-03-21 11:28:03 -0700232 root.flags |= Root.FLAG_ADVANCED;
Steve McKayefa17612016-01-29 18:15:39 -0800233 }
Steve McKayc6a4cd82015-11-18 14:56:50 -0800234 // Dunno when this would NOT be the case, but never hurts to be correct.
235 if (volume.isMountedWritable()) {
236 root.flags |= Root.FLAG_SUPPORTS_CREATE;
237 }
238 root.title = title;
239 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
240 root.flags |= Root.FLAG_HAS_SETTINGS;
241 }
242 if (volume.isVisibleForRead(userId)) {
243 root.visiblePath = volume.getPathForUser(userId);
244 } else {
245 root.visiblePath = null;
246 }
247 root.path = volume.getInternalPathForUser(userId);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700248 try {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700249 root.docId = getDocIdForFile(root.path);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800250 } catch (FileNotFoundException e) {
251 throw new IllegalStateException(e);
252 }
253 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700254
Steve McKayecec7cb2016-03-02 11:35:39 -0800255 // Finally, if primary storage is available we add the "Documents" directory.
256 // If I recall correctly the actual directory is created on demand
257 // by calling either getPathForUser, or getInternalPathForUser.
Steve McKayc6a4cd82015-11-18 14:56:50 -0800258 if (primaryVolume != null && primaryVolume.isVisible()) {
259 final RootInfo root = new RootInfo();
260 root.rootId = ROOT_ID_HOME;
261 mRoots.put(root.rootId, root);
Steve McKayab3b8932016-02-16 11:37:03 -0800262 root.title = getContext().getString(R.string.root_documents);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800263
264 // Only report bytes on *volumes*...as a matter of policy.
265 root.reportAvailableBytes = false;
266 root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH
267 | Root.FLAG_SUPPORTS_IS_CHILD;
268
269 // Dunno when this would NOT be the case, but never hurts to be correct.
270 if (primaryVolume.isMountedWritable()) {
271 root.flags |= Root.FLAG_SUPPORTS_CREATE;
272 }
273
Steve McKayecec7cb2016-03-02 11:35:39 -0800274 // Create the "Documents" directory on disk (don't use the localized title).
Steve McKayc6a4cd82015-11-18 14:56:50 -0800275 root.visiblePath = new File(
Steve McKayab3b8932016-02-16 11:37:03 -0800276 primaryVolume.getPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800277 root.path = new File(
Steve McKayab3b8932016-02-16 11:37:03 -0800278 primaryVolume.getInternalPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800279 try {
280 root.docId = getDocIdForFile(root.path);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700281 } catch (FileNotFoundException e) {
282 throw new IllegalStateException(e);
283 }
284 }
285
286 Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
287
Makoto Onuki14a6df72015-07-01 14:55:14 -0700288 // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
289 // as well as content://com.android.externalstorage.documents/document/*/children,
290 // so just notify on content://com.android.externalstorage.documents/.
291 getContext().getContentResolver().notifyChange(BASE_URI, null, false);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700292 }
293
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700294 private static String[] resolveRootProjection(String[] projection) {
295 return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
296 }
297
Garfield Tan75379db2017-02-08 15:32:56 -0800298 @Override
299 protected String getDocIdForFile(File file) throws FileNotFoundException {
Felipe Lemeb012f912016-01-22 16:49:55 -0800300 return getDocIdForFileMaybeCreate(file, false);
301 }
302
303 private String getDocIdForFileMaybeCreate(File file, boolean createNewDir)
304 throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700305 String path = file.getAbsolutePath();
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700306
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700307 // Find the most-specific root path
Garfield Tandc9593e2017-01-09 18:04:43 -0800308 boolean visiblePath = false;
309 RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false);
310
311 if (mostSpecificRoot == null) {
312 // Try visible path if no internal path matches. MediaStore uses visible paths.
313 visiblePath = true;
314 mostSpecificRoot = getMostSpecificRootForPath(path, true);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700315 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700316
Garfield Tandc9593e2017-01-09 18:04:43 -0800317 if (mostSpecificRoot == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700318 throw new FileNotFoundException("Failed to find root that contains " + path);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700319 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700320
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700321 // Start at first char of path under root
Garfield Tandc9593e2017-01-09 18:04:43 -0800322 final String rootPath = visiblePath
323 ? mostSpecificRoot.visiblePath.getAbsolutePath()
324 : mostSpecificRoot.path.getAbsolutePath();
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700325 if (rootPath.equals(path)) {
326 path = "";
327 } else if (rootPath.endsWith("/")) {
328 path = path.substring(rootPath.length());
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700329 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700330 path = path.substring(rootPath.length() + 1);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700331 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700332
Felipe Lemeb012f912016-01-22 16:49:55 -0800333 if (!file.exists() && createNewDir) {
334 Log.i(TAG, "Creating new directory " + file);
335 if (!file.mkdir()) {
336 Log.e(TAG, "Could not create directory " + file);
337 }
338 }
339
Garfield Tandc9593e2017-01-09 18:04:43 -0800340 return mostSpecificRoot.rootId + ':' + path;
341 }
342
343 private RootInfo getMostSpecificRootForPath(String path, boolean visible) {
344 // Find the most-specific root path
345 RootInfo mostSpecificRoot = null;
346 String mostSpecificPath = null;
347 synchronized (mRootsLock) {
348 for (int i = 0; i < mRoots.size(); i++) {
349 final RootInfo root = mRoots.valueAt(i);
350 final File rootFile = visible ? root.visiblePath : root.path;
351 if (rootFile != null) {
352 final String rootPath = rootFile.getAbsolutePath();
353 if (path.startsWith(rootPath) && (mostSpecificPath == null
354 || rootPath.length() > mostSpecificPath.length())) {
355 mostSpecificRoot = root;
356 mostSpecificPath = rootPath;
357 }
358 }
359 }
360 }
361
362 return mostSpecificRoot;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700363 }
364
Garfield Tan75379db2017-02-08 15:32:56 -0800365 @Override
366 protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600367 return getFileForDocId(docId, visible, true);
368 }
369
370 private File getFileForDocId(String docId, boolean visible, boolean mustExist)
371 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700372 RootInfo root = getRootFromDocId(docId);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600373 return buildFile(root, docId, visible, mustExist);
Garfield Tanaba97f32016-10-06 17:34:19 +0000374 }
375
376 private Pair<RootInfo, File> resolveDocId(String docId, boolean visible)
377 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700378 RootInfo root = getRootFromDocId(docId);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600379 return Pair.create(root, buildFile(root, docId, visible, true));
Garfield Tan06940e12016-10-07 16:03:17 -0700380 }
381
382 private RootInfo getRootFromDocId(String docId) throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700383 final int splitIndex = docId.indexOf(':', 1);
384 final String tag = docId.substring(0, splitIndex);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700385
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700386 RootInfo root;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700387 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700388 root = mRoots.get(tag);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700389 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700390 if (root == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700391 throw new FileNotFoundException("No root for " + tag);
392 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700393
Garfield Tan06940e12016-10-07 16:03:17 -0700394 return root;
395 }
396
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600397 private File buildFile(RootInfo root, String docId, boolean visible, boolean mustExist)
Garfield Tan06940e12016-10-07 16:03:17 -0700398 throws FileNotFoundException {
399 final int splitIndex = docId.indexOf(':', 1);
400 final String path = docId.substring(splitIndex + 1);
401
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700402 File target = visible ? root.visiblePath : root.path;
403 if (target == null) {
404 return null;
405 }
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700406 if (!target.exists()) {
407 target.mkdirs();
408 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700409 target = new File(target, path);
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600410 if (mustExist && !target.exists()) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700411 throw new FileNotFoundException("Missing file for " + docId + " at " + target);
412 }
Garfield Tan06940e12016-10-07 16:03:17 -0700413 return target;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700414 }
415
Garfield Tan75379db2017-02-08 15:32:56 -0800416 @Override
417 protected Uri buildNotificationUri(String docId) {
418 return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700419 }
420
421 @Override
Jeff Sharkeyb00d5ea2018-05-01 10:01:52 -0600422 protected void onDocIdChanged(String docId) {
423 try {
424 // Touch the visible path to ensure that any sdcardfs caches have
425 // been updated to reflect underlying changes on disk.
426 final File visiblePath = getFileForDocId(docId, true, false);
427 if (visiblePath != null) {
428 Os.access(visiblePath.getAbsolutePath(), OsConstants.F_OK);
429 }
430 } catch (FileNotFoundException | ErrnoException ignored) {
431 }
432 }
433
434 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700435 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
436 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700437 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700438 for (RootInfo root : mRoots.values()) {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700439 final RowBuilder row = result.newRow();
440 row.add(Root.COLUMN_ROOT_ID, root.rootId);
441 row.add(Root.COLUMN_FLAGS, root.flags);
442 row.add(Root.COLUMN_TITLE, root.title);
443 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
Jeff Sharkey06823d42017-05-09 16:55:29 -0600444
445 long availableBytes = -1;
446 if (root.reportAvailableBytes) {
447 if (root.storageUuid != null) {
448 try {
449 availableBytes = getContext()
450 .getSystemService(StorageStatsManager.class)
451 .getFreeBytes(root.storageUuid);
452 } catch (IOException e) {
453 Log.w(TAG, e);
454 }
455 } else {
456 availableBytes = root.path.getUsableSpace();
457 }
458 }
459 row.add(Root.COLUMN_AVAILABLE_BYTES, availableBytes);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700460 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700461 }
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700462 return result;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700463 }
464
465 @Override
Garfield Tanb690b4d2017-03-01 16:05:23 -0800466 public Path findDocumentPath(@Nullable String parentDocId, String childDocId)
Garfield Tanaba97f32016-10-06 17:34:19 +0000467 throws FileNotFoundException {
Garfield Tan06940e12016-10-07 16:03:17 -0700468 final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false);
469 final RootInfo root = resolvedDocId.first;
470 File child = resolvedDocId.second;
Garfield Tanaba97f32016-10-06 17:34:19 +0000471
Garfield Tan06940e12016-10-07 16:03:17 -0700472 final File parent = TextUtils.isEmpty(parentDocId)
473 ? root.path
474 : getFileForDocId(parentDocId);
475
Garfield Tan75379db2017-02-08 15:32:56 -0800476 return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child));
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700477 }
478
Garfield Tan92b96ba2016-11-01 14:33:48 -0700479 private Uri getDocumentUri(String path, List<UriPermission> accessUriPermissions)
480 throws FileNotFoundException {
481 File doc = new File(path);
482
483 final String docId = getDocIdForFile(doc);
484
485 UriPermission docUriPermission = null;
486 UriPermission treeUriPermission = null;
487 for (UriPermission uriPermission : accessUriPermissions) {
488 final Uri uri = uriPermission.getUri();
489 if (AUTHORITY.equals(uri.getAuthority())) {
490 boolean matchesRequestedDoc = false;
491 if (DocumentsContract.isTreeUri(uri)) {
492 final String parentDocId = DocumentsContract.getTreeDocumentId(uri);
Garfield Tandc9593e2017-01-09 18:04:43 -0800493 if (isChildDocument(parentDocId, docId)) {
Garfield Tan92b96ba2016-11-01 14:33:48 -0700494 treeUriPermission = uriPermission;
495 matchesRequestedDoc = true;
496 }
497 } else {
498 final String candidateDocId = DocumentsContract.getDocumentId(uri);
Garfield Tandc9593e2017-01-09 18:04:43 -0800499 if (Objects.equals(docId, candidateDocId)) {
Garfield Tan92b96ba2016-11-01 14:33:48 -0700500 docUriPermission = uriPermission;
501 matchesRequestedDoc = true;
502 }
503 }
504
505 if (matchesRequestedDoc && allowsBothReadAndWrite(uriPermission)) {
506 // This URI permission provides everything an app can get, no need to
507 // further check any other granted URI.
508 break;
509 }
510 }
511 }
512
513 // Full permission URI first.
514 if (allowsBothReadAndWrite(treeUriPermission)) {
515 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
516 }
517
518 if (allowsBothReadAndWrite(docUriPermission)) {
519 return docUriPermission.getUri();
520 }
521
522 // Then partial permission URI.
523 if (treeUriPermission != null) {
524 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
525 }
526
527 if (docUriPermission != null) {
528 return docUriPermission.getUri();
529 }
530
531 throw new SecurityException("The app is not given any access to the document under path " +
532 path + " with permissions granted in " + accessUriPermissions);
533 }
534
535 private static boolean allowsBothReadAndWrite(UriPermission permission) {
536 return permission != null
537 && permission.isReadPermission()
538 && permission.isWritePermission();
539 }
540
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700541 @Override
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700542 public Cursor querySearchDocuments(String rootId, String query, String[] projection)
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700543 throws FileNotFoundException {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700544 final File parent;
545 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700546 parent = mRoots.get(rootId).path;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700547 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700548
Garfield Tan75379db2017-02-08 15:32:56 -0800549 return querySearchDocuments(parent, query, projection, Collections.emptySet());
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700550 }
551
552 @Override
Garfield Tan87877032017-03-22 12:01:14 -0700553 public void ejectRoot(String rootId) {
Ben Line7822fb2016-06-24 15:21:08 -0700554 final long token = Binder.clearCallingIdentity();
Ben Line7822fb2016-06-24 15:21:08 -0700555 RootInfo root = mRoots.get(rootId);
556 if (root != null) {
557 try {
558 mStorageManager.unmount(root.volumeId);
Ben Line7822fb2016-06-24 15:21:08 -0700559 } catch (RuntimeException e) {
Garfield Tan87877032017-03-22 12:01:14 -0700560 throw new IllegalStateException(e);
Ben Line7822fb2016-06-24 15:21:08 -0700561 } finally {
562 Binder.restoreCallingIdentity(token);
563 }
564 }
Ben Line7822fb2016-06-24 15:21:08 -0700565 }
566
567 @Override
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700568 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
569 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160);
570 synchronized (mRootsLock) {
571 for (int i = 0; i < mRoots.size(); i++) {
572 final RootInfo root = mRoots.valueAt(i);
573 pw.println("Root{" + root.rootId + "}:");
574 pw.increaseIndent();
575 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
576 pw.println();
577 pw.printPair("title", root.title);
578 pw.printPair("docId", root.docId);
579 pw.println();
580 pw.printPair("path", root.path);
581 pw.printPair("visiblePath", root.visiblePath);
582 pw.decreaseIndent();
583 pw.println();
584 }
585 }
586 }
587
Felipe Lemeb012f912016-01-22 16:49:55 -0800588 @Override
589 public Bundle call(String method, String arg, Bundle extras) {
590 Bundle bundle = super.call(method, arg, extras);
591 if (bundle == null && !TextUtils.isEmpty(method)) {
592 switch (method) {
593 case "getDocIdForFileCreateNewDir": {
594 getContext().enforceCallingPermission(
595 android.Manifest.permission.MANAGE_DOCUMENTS, null);
596 if (TextUtils.isEmpty(arg)) {
597 return null;
598 }
599 try {
600 final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
601 bundle = new Bundle();
602 bundle.putString("DOC_ID", docId);
603 } catch (FileNotFoundException e) {
604 Log.w(TAG, "file '" + arg + "' not found");
605 return null;
606 }
607 break;
608 }
Garfield Tan92b96ba2016-11-01 14:33:48 -0700609 case "getDocumentId": {
610 final String path = arg;
611 final List<UriPermission> accessUriPermissions =
612 extras.getParcelableArrayList(AUTHORITY + ".extra.uriPermissions");
613
614 try {
615 final Bundle out = new Bundle();
616 final Uri uri = getDocumentUri(path, accessUriPermissions);
617 out.putParcelable(DocumentsContract.EXTRA_URI, uri);
618 return out;
619 } catch (FileNotFoundException e) {
620 throw new IllegalStateException("File in " + path + " is not found.", e);
621 }
622
623 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800624 default:
625 Log.w(TAG, "unknown method passed to call(): " + method);
626 }
627 }
628 return bundle;
629 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700630}