Home | History | Annotate | Download | only in base
      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