Home | History | Annotate | Download | only in documentsui
      1 /*
      2  * Copyright (C) 2016 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;
     18 
     19 import static android.os.Environment.isStandardDirectory;
     20 import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
     21 import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
     22 
     23 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED;
     24 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED;
     25 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_DENIED;
     26 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST;
     27 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ERROR;
     28 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_GRANTED;
     29 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS;
     30 import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY;
     31 import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccessRequest;
     32 import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest;
     33 import static com.android.documentsui.base.SharedMinimal.DEBUG;
     34 import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
     35 import static com.android.documentsui.base.SharedMinimal.getUriPermission;
     36 import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
     37 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
     38 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
     39 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getScopedAccessPermissionStatus;
     40 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus;
     41 
     42 import android.annotation.Nullable;
     43 import android.annotation.SuppressLint;
     44 import android.app.Activity;
     45 import android.app.ActivityManager;
     46 import android.app.AlertDialog;
     47 import android.app.Dialog;
     48 import android.app.DialogFragment;
     49 import android.app.FragmentManager;
     50 import android.app.FragmentTransaction;
     51 import android.app.GrantedUriPermission;
     52 import android.content.ContentProviderClient;
     53 import android.content.Context;
     54 import android.content.DialogInterface;
     55 import android.content.DialogInterface.OnClickListener;
     56 import android.content.Intent;
     57 import android.content.UriPermission;
     58 import android.content.pm.PackageManager;
     59 import android.content.pm.PackageManager.NameNotFoundException;
     60 import android.net.Uri;
     61 import android.os.Bundle;
     62 import android.os.Parcelable;
     63 import android.os.RemoteException;
     64 import android.os.UserHandle;
     65 import android.os.storage.StorageManager;
     66 import android.os.storage.StorageVolume;
     67 import android.os.storage.VolumeInfo;
     68 import android.provider.DocumentsContract;
     69 import android.text.TextUtils;
     70 import android.util.Log;
     71 import android.view.View;
     72 import android.widget.CheckBox;
     73 import android.widget.CompoundButton;
     74 import android.widget.CompoundButton.OnCheckedChangeListener;
     75 import android.widget.TextView;
     76 
     77 import com.android.documentsui.base.Providers;
     78 
     79 import java.io.File;
     80 import java.io.IOException;
     81 import java.util.List;
     82 
     83 /**
     84  * Activity responsible for handling {@link StorageVolume#createAccessIntent(String)}.
     85  */
     86 public class ScopedAccessActivity extends Activity {
     87     private static final String TAG = "ScopedAccessActivity";
     88     private static final String FM_TAG = "open_external_directory";
     89     private static final String EXTRA_FILE = "com.android.documentsui.FILE";
     90     private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
     91     private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
     92     private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID";
     93     private static final String EXTRA_IS_ROOT = "com.android.documentsui.IS_ROOT";
     94     private static final String EXTRA_IS_PRIMARY = "com.android.documentsui.IS_PRIMARY";
     95 
     96     private ContentProviderClient mExternalStorageClient;
     97 
     98     @Override
     99     public void onCreate(Bundle savedInstanceState) {
    100         super.onCreate(savedInstanceState);
    101         if (savedInstanceState != null) {
    102             if (DEBUG) Log.d(TAG, "activity.onCreateDialog(): reusing instance");
    103             return;
    104         }
    105 
    106         final Intent intent = getIntent();
    107         if (intent == null) {
    108             if (DEBUG) Log.d(TAG, "missing intent");
    109             logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
    110             setResult(RESULT_CANCELED);
    111             finish();
    112             return;
    113         }
    114         final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
    115         if (!(storageVolume instanceof StorageVolume)) {
    116             if (DEBUG)
    117                 Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
    118                         + storageVolume);
    119             logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
    120             setResult(RESULT_CANCELED);
    121             finish();
    122             return;
    123         }
    124         String directoryName =
    125                 getInternalDirectoryName(intent.getStringExtra(EXTRA_DIRECTORY_NAME));
    126         final StorageVolume volume = (StorageVolume) storageVolume;
    127         if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(),
    128                 volume.getUuid(), directoryName) == PERMISSION_NEVER_ASK) {
    129             logValidScopedAccessRequest(this, directoryName,
    130                     SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED);
    131             setResult(RESULT_CANCELED);
    132             finish();
    133             return;
    134         }
    135 
    136         final int userId = UserHandle.myUserId();
    137         if (!showFragment(this, userId, volume, directoryName)) {
    138             setResult(RESULT_CANCELED);
    139             finish();
    140             return;
    141         }
    142     }
    143 
    144     @Override
    145     public void onDestroy() {
    146         super.onDestroy();
    147         if (mExternalStorageClient != null) {
    148             mExternalStorageClient.close();
    149         }
    150     }
    151 
    152     /**
    153      * Validates the given path (volume + directory) and display the appropriate dialog asking the
    154      * user to grant access to it.
    155      */
    156     private static boolean showFragment(ScopedAccessActivity activity, int userId,
    157             StorageVolume storageVolume, String directoryName) {
    158         return getUriPermission(activity,
    159                 activity.getExternalStorageClient(), storageVolume, directoryName, userId, true,
    160                 (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> {
    161                     // Checks if the user has granted the permission already.
    162                     final Intent intent = getIntentForExistingPermission(activity,
    163                             activity.getCallingPackage(), grantedUri, rootUri);
    164                     if (intent != null) {
    165                         logValidScopedAccessRequest(activity, isRoot ? "." : directoryName,
    166                                 SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED);
    167                         activity.setResult(RESULT_OK, intent);
    168                         activity.finish();
    169                         return true;
    170                     }
    171 
    172                     // Gets the package label.
    173                     final String appLabel = getAppLabel(activity);
    174                     if (appLabel == null) {
    175                         // Error already logged.
    176                         return false;
    177                     }
    178 
    179                     // Sets args that will be retrieve on onCreate()
    180                     final Bundle args = new Bundle();
    181                     args.putString(EXTRA_FILE, file.getAbsolutePath());
    182                     args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
    183                     args.putString(EXTRA_VOLUME_UUID, storageVolume.getUuid());
    184                     args.putString(EXTRA_APP_LABEL, appLabel);
    185                     args.putBoolean(EXTRA_IS_ROOT, isRoot);
    186                     args.putBoolean(EXTRA_IS_PRIMARY, isPrimary);
    187 
    188                     final FragmentManager fm = activity.getFragmentManager();
    189                     final FragmentTransaction ft = fm.beginTransaction();
    190                     final ScopedAccessDialogFragment fragment = new ScopedAccessDialogFragment();
    191                     fragment.setArguments(args);
    192                     ft.add(fragment, FM_TAG);
    193                     ft.commitAllowingStateLoss();
    194 
    195                     return true;
    196                 });
    197     }
    198 
    199     private static String getAppLabel(Activity activity) {
    200         final String packageName = activity.getCallingPackage();
    201         final PackageManager pm = activity.getPackageManager();
    202         try {
    203             return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
    204         } catch (NameNotFoundException e) {
    205             logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
    206             Log.w(TAG, "Could not get label for package " + packageName);
    207             return null;
    208         }
    209     }
    210 
    211     private static Intent createGrantedUriPermissionsIntent(Context context,
    212             ContentProviderClient provider, File file) {
    213         final Uri uri = getUriPermission(context, provider, file);
    214         return createGrantedUriPermissionsIntent(uri);
    215     }
    216 
    217     private static Intent createGrantedUriPermissionsIntent(Uri uri) {
    218         final Intent intent = new Intent();
    219         intent.setData(uri);
    220         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
    221                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    222                 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
    223                 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
    224         return intent;
    225     }
    226 
    227     private static Intent getIntentForExistingPermission(Context context, String packageName,
    228             Uri grantedUri, Uri rootUri) {
    229         if (DEBUG) {
    230             Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri
    231                     + " or its root (" + rootUri + ")");
    232         }
    233         final ActivityManager am = context.getSystemService(ActivityManager.class);
    234         for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName)
    235                 .getList()) {
    236             final Uri uri = uriPermission.uri;
    237             if (uri == null) {
    238                 Log.w(TAG, "null URI for " + uriPermission);
    239                 continue;
    240             }
    241             if (uri.equals(grantedUri) || uri.equals(rootUri)) {
    242                 if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
    243                 return createGrantedUriPermissionsIntent(grantedUri);
    244             }
    245         }
    246         if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
    247         return null;
    248     }
    249 
    250     public static class ScopedAccessDialogFragment extends DialogFragment {
    251 
    252         private File mFile;
    253         private String mVolumeUuid;
    254         private String mVolumeLabel;
    255         private String mAppLabel;
    256         private boolean mIsRoot;
    257         private boolean mIsPrimary;
    258         private CheckBox mDontAskAgain;
    259         private ScopedAccessActivity mActivity;
    260         private AlertDialog mDialog;
    261 
    262         @Override
    263         public void onCreate(Bundle savedInstanceState) {
    264             super.onCreate(savedInstanceState);
    265             setRetainInstance(true);
    266             final Bundle args = getArguments();
    267             if (args != null) {
    268                 mFile = new File(args.getString(EXTRA_FILE));
    269                 mVolumeUuid = args.getString(EXTRA_VOLUME_UUID);
    270                 mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
    271                 mAppLabel = args.getString(EXTRA_APP_LABEL);
    272                 mIsRoot = args.getBoolean(EXTRA_IS_ROOT);
    273                 mIsPrimary= args.getBoolean(EXTRA_IS_PRIMARY);
    274             }
    275             mActivity = (ScopedAccessActivity) getActivity();
    276         }
    277 
    278         @Override
    279         public void onDestroyView() {
    280             // Workaround for https://code.google.com/p/android/issues/detail?id=17423
    281             if (mDialog != null && getRetainInstance()) {
    282                 mDialog.setDismissMessage(null);
    283             }
    284             super.onDestroyView();
    285         }
    286 
    287         @Override
    288         public Dialog onCreateDialog(Bundle savedInstanceState) {
    289             if (mDialog != null) {
    290                 if (DEBUG) Log.d(TAG, "fragment.onCreateDialog(): reusing dialog");
    291                 return mDialog;
    292             }
    293             if (mActivity != getActivity()) {
    294                 // Sanity check.
    295                 Log.wtf(TAG, "activity references don't match on onCreateDialog(): mActivity = "
    296                         + mActivity + " , getActivity() = " + getActivity());
    297                 mActivity = (ScopedAccessActivity) getActivity();
    298             }
    299             final String directory = mFile.getName();
    300             final String directoryName = mIsRoot ? DIRECTORY_ROOT : directory;
    301             final Context context = mActivity.getApplicationContext();
    302             final OnClickListener listener = new OnClickListener() {
    303 
    304                 @Override
    305                 public void onClick(DialogInterface dialog, int which) {
    306                     Intent intent = null;
    307                     if (which == DialogInterface.BUTTON_POSITIVE) {
    308                         intent = createGrantedUriPermissionsIntent(mActivity,
    309                                 mActivity.getExternalStorageClient(), mFile);
    310                     }
    311                     if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
    312                         logValidScopedAccessRequest(mActivity, directoryName,
    313                                 SCOPED_DIRECTORY_ACCESS_DENIED);
    314                         final boolean checked = mDontAskAgain.isChecked();
    315                         if (checked) {
    316                             logValidScopedAccessRequest(mActivity, directory,
    317                                     SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST);
    318                             setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
    319                                     mVolumeUuid, directoryName, PERMISSION_NEVER_ASK);
    320                         } else {
    321                             setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
    322                                     mVolumeUuid, directoryName, PERMISSION_ASK_AGAIN);
    323                         }
    324                         mActivity.setResult(RESULT_CANCELED);
    325                     } else {
    326                         logValidScopedAccessRequest(mActivity, directory,
    327                                 SCOPED_DIRECTORY_ACCESS_GRANTED);
    328                         mActivity.setResult(RESULT_OK, intent);
    329                     }
    330                     mActivity.finish();
    331                 }
    332             };
    333 
    334             @SuppressLint("InflateParams")
    335             // It's ok pass null ViewRoot on AlertDialogs.
    336             final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null);
    337             final CharSequence message;
    338             if (mIsRoot) {
    339                 message = TextUtils.expandTemplate(getText(
    340                         R.string.open_external_dialog_root_request), mAppLabel, mVolumeLabel);
    341             } else {
    342                 message = TextUtils.expandTemplate(
    343                         getText(mIsPrimary ? R.string.open_external_dialog_request_primary_volume
    344                                 : R.string.open_external_dialog_request),
    345                                 mAppLabel, directory, mVolumeLabel);
    346             }
    347             final TextView messageField = (TextView) view.findViewById(R.id.message);
    348             messageField.setText(message);
    349             mDialog = new AlertDialog.Builder(mActivity, R.style.Theme_AppCompat_Light_Dialog_Alert)
    350                     .setView(view)
    351                     .setPositiveButton(R.string.allow, listener)
    352                     .setNegativeButton(R.string.deny, listener)
    353                     .create();
    354 
    355             mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox);
    356             if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
    357                     mVolumeUuid, directoryName) == PERMISSION_ASK_AGAIN) {
    358                 mDontAskAgain.setVisibility(View.VISIBLE);
    359                 mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    360 
    361                     @Override
    362                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    363                         mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked);
    364                     }
    365                 });
    366             }
    367 
    368             return mDialog;
    369         }
    370 
    371         @Override
    372         public void onCancel(DialogInterface dialog) {
    373             super.onCancel(dialog);
    374             final Activity activity = getActivity();
    375             logValidScopedAccessRequest(activity, mFile.getName(), SCOPED_DIRECTORY_ACCESS_DENIED);
    376             activity.setResult(RESULT_CANCELED);
    377             activity.finish();
    378         }
    379     }
    380 
    381     private synchronized ContentProviderClient getExternalStorageClient() {
    382         if (mExternalStorageClient == null) {
    383             mExternalStorageClient =
    384                     getContentResolver().acquireContentProviderClient(Providers.AUTHORITY_STORAGE);
    385         }
    386         return mExternalStorageClient;
    387     }
    388 }
    389