1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.documentsui.base; 18 19 import static android.os.Environment.isStandardDirectory; 20 21 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ERROR; 22 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; 23 import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccessRequest; 24 25 import android.annotation.Nullable; 26 import android.content.ContentProviderClient; 27 import android.content.Context; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.RemoteException; 32 import android.os.storage.StorageManager; 33 import android.os.storage.StorageVolume; 34 import android.os.storage.VolumeInfo; 35 import android.provider.DocumentsContract; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import java.io.File; 40 import java.io.IOException; 41 import java.util.List; 42 43 /** 44 * Contains the minimum number of utilities (contants, helpers, etc...) that can be used by both the 45 * main package and the minimal APK that's used by Android TV (and other devices). 46 * 47 * <p>In other words, it should not include any external dependency that would increase the APK 48 * size. 49 */ 50 public final class SharedMinimal { 51 52 public static final String TAG = "Documents"; 53 54 public static final boolean DEBUG = Build.IS_DEBUGGABLE; 55 public static final boolean VERBOSE = DEBUG && Log.isLoggable(TAG, Log.VERBOSE); 56 57 /** 58 * Special directory name representing the full volume of a scoped directory request. 59 */ 60 public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY"; 61 62 /** 63 * Callback for {@link SharedMinimal#getUriPermission(Context, ContentProviderClient, 64 * StorageVolume, String, int, boolean, GetUriPermissionCallback)}. 65 */ 66 public static interface GetUriPermissionCallback { 67 68 /** 69 * Evaluates the result of the request. 70 * 71 * @param file the path of the requested URI. 72 * @param volumeLabel user-friendly label of the volume. 73 * @param isRoot whether the requested directory is the root directory. 74 * @param isPrimary whether the requested volume is the primary storage volume. 75 * @param requestedUri the requested URI. 76 * @param rootUri the URI for the volume's root directory. 77 * @return whethe the result was sucessfully. 78 */ 79 boolean onResult(File file, String volumeLabel, boolean isRoot, boolean isPrimary, 80 Uri requestedUri, Uri rootUri); 81 } 82 83 /** 84 * Gets the name of a directory name in the format that's used internally by the app 85 * (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT}); 86 * if necessary. 87 */ 88 public static String getInternalDirectoryName(@Nullable String name) { 89 return name == null ? DIRECTORY_ROOT : name; 90 } 91 92 /** 93 * Gets the name of a directory name in the format that is used externally 94 * (i.e., mapping {@link #DIRECTORY_ROOT} to {@code null} if necessary); 95 */ 96 @Nullable 97 public static String getExternalDirectoryName(String name) { 98 return name.equals(DIRECTORY_ROOT) ? null : name; 99 } 100 101 /** 102 * Gets the URI permission for the given volume and directory. 103 * 104 * @param context caller's context. 105 * @param storageClient storage provider client. 106 * @param storageVolume volume. 107 * @param directoryName directory name, or {@link #DIRECTORY_ROOT} for full volume. 108 * @param userId caller's user handle. 109 * @param logMetrics whether intermediate errors should be logged. 110 * @param callback callback that receives the results. 111 * 112 * @return whether the call was succesfull or not. 113 */ 114 public static boolean getUriPermission(Context context, 115 ContentProviderClient storageClient, StorageVolume storageVolume, 116 String directoryName, int userId, boolean logMetrics, 117 GetUriPermissionCallback callback) { 118 if (DEBUG) { 119 Log.d(TAG, "getUriPermission() for volume " + storageVolume.dump() + ", directory " 120 + directoryName + ", and user " + userId); 121 } 122 final boolean isRoot = directoryName.equals(DIRECTORY_ROOT); 123 final boolean isPrimary = storageVolume.isPrimary(); 124 125 if (isRoot && isPrimary) { 126 if (DEBUG) Log.d(TAG, "root access requested on primary volume"); 127 return false; 128 } 129 130 final File volumeRoot = storageVolume.getPathFile(); 131 File file; 132 try { 133 file = isRoot ? volumeRoot : new File(volumeRoot, directoryName).getCanonicalFile(); 134 } catch (IOException e) { 135 Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump() 136 + " and directory " + directoryName); 137 if (logMetrics) logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); 138 return false; 139 } 140 final StorageManager sm = context.getSystemService(StorageManager.class); 141 142 final String root, directory; 143 if (isRoot) { 144 root = volumeRoot.getAbsolutePath(); 145 directory = "."; 146 } else { 147 root = file.getParent(); 148 directory = file.getName(); 149 // Verify directory is valid. 150 if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) { 151 if (DEBUG) { 152 Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '" 153 + file.getAbsolutePath() + "')"); 154 } 155 if (logMetrics) { 156 logInvalidScopedAccessRequest(context, 157 SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY); 158 } 159 return false; 160 } 161 } 162 163 // Gets volume label and converted path. 164 String volumeLabel = null; 165 final List<VolumeInfo> volumes = sm.getVolumes(); 166 if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); 167 File internalRoot = null; 168 for (VolumeInfo volume : volumes) { 169 if (isRightVolume(volume, root, userId)) { 170 internalRoot = volume.getInternalPathForUser(userId); 171 // Must convert path before calling getDocIdForFileCreateNewDir() 172 if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); 173 file = isRoot ? internalRoot : new File(internalRoot, directory); 174 volumeLabel = sm.getBestVolumeDescription(volume); 175 if (TextUtils.isEmpty(volumeLabel)) { 176 volumeLabel = storageVolume.getDescription(context); 177 } 178 if (TextUtils.isEmpty(volumeLabel)) { 179 volumeLabel = context.getString(android.R.string.unknownName); 180 Log.w(TAG, "No volume description for " + volume + "; using " + volumeLabel); 181 } 182 break; 183 } 184 } 185 if (internalRoot == null) { 186 // Should not happen on normal circumstances, unless app crafted an invalid volume 187 // using reflection or the list of mounted volumes changed. 188 Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes); 189 return false; 190 } 191 192 final Uri requestedUri = getUriPermission(context, storageClient, file); 193 final Uri rootUri = internalRoot.equals(file) ? requestedUri 194 : getUriPermission(context, storageClient, internalRoot); 195 196 return callback.onResult(file, volumeLabel, isRoot, isPrimary, requestedUri, rootUri); 197 } 198 199 /** 200 * Creates an URI permission for the given file. 201 */ 202 public static Uri getUriPermission(Context context, ContentProviderClient storageProvider, 203 File file) { 204 // Calls ExternalStorageProvider to get the doc id for the file 205 final Bundle bundle; 206 try { 207 bundle = storageProvider.call("getDocIdForFileCreateNewDir", file.getPath(), null); 208 } catch (RemoteException e) { 209 Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e); 210 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); 211 return null; 212 } 213 final String docId = bundle == null ? null : bundle.getString("DOC_ID"); 214 if (docId == null) { 215 Log.e(TAG, "Did not get doc id from External Storage provider for " + file); 216 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); 217 return null; 218 } 219 if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId); 220 221 final Uri uri = DocumentsContract.buildTreeDocumentUri(Providers.AUTHORITY_STORAGE, docId); 222 if (uri == null) { 223 Log.e(TAG, "Could not get URI for doc id " + docId); 224 return null; 225 } 226 if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri); 227 return uri; 228 } 229 230 private static boolean isRightVolume(VolumeInfo volume, String root, int userId) { 231 final File userPath = volume.getPathForUser(userId); 232 final String path = userPath == null ? null : volume.getPathForUser(userId).getPath(); 233 final boolean isMounted = volume.isMountedReadable(); 234 if (DEBUG) 235 Log.d(TAG, "Volume: " + volume 236 + "\n\tuserId: " + userId 237 + "\n\tuserPath: " + userPath 238 + "\n\troot: " + root 239 + "\n\tpath: " + path 240 + "\n\tisMounted: " + isMounted); 241 242 return isMounted && root.equals(path); 243 } 244 245 private SharedMinimal() { 246 throw new UnsupportedOperationException("provides static fields only"); 247 } 248 } 249