• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedInstanceState)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
onDestroy()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      */
showFragment(ScopedAccessActivity activity, int userId, StorageVolume storageVolume, String directoryName)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 
getAppLabel(Activity activity)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 
createGrantedUriPermissionsIntent(Context context, ContentProviderClient provider, File file)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 
createGrantedUriPermissionsIntent(Uri uri)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 
getIntentForExistingPermission(Context context, String packageName, Uri grantedUri, Uri rootUri)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
onCreate(Bundle savedInstanceState)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
onDestroyView()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
onCreateDialog(Bundle savedInstanceState)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
onCancel(DialogInterface dialog)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 
getExternalStorageClient()381     private synchronized ContentProviderClient getExternalStorageClient() {
382         if (mExternalStorageClient == null) {
383             mExternalStorageClient =
384                     getContentResolver().acquireContentProviderClient(Providers.AUTHORITY_STORAGE);
385         }
386         return mExternalStorageClient;
387     }
388 }
389