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