• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onResult(File file, String volumeLabel, boolean isRoot, boolean isPrimary, Uri requestedUri, Uri rootUri)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      */
getInternalDirectoryName(@ullable String name)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
getExternalDirectoryName(String name)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      */
getUriPermission(Context context, ContentProviderClient storageClient, StorageVolume storageVolume, String directoryName, int userId, boolean logMetrics, GetUriPermissionCallback callback)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      */
getUriPermission(Context context, ContentProviderClient storageProvider, File file)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 
isRightVolume(VolumeInfo volume, String root, int userId)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 
SharedMinimal()245     private SharedMinimal() {
246         throw new UnsupportedOperationException("provides static fields only");
247     }
248 }
249