blob: 77e76f9d23778febdeb325e5e983bbd384700e8b [file] [log] [blame]
Felipe Lemea4f4d7e2016-01-22 16:49:55 -08001/*
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
17package com.android.documentsui;
18
19import static android.os.Environment.isStandardDirectory;
Felipe Leme5228bd02016-02-17 10:12:04 -080020import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
21import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080022import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED;
Felipe Lemee4b60122016-02-24 10:17:41 -080023import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED;
24import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080025import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST;
Felipe Lemee4b60122016-02-24 10:17:41 -080026import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ERROR;
27import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_GRANTED;
28import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS;
29import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080030import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest;
31import static com.android.documentsui.Metrics.logValidScopedAccessRequest;
Steve McKayd9caa6a2016-09-15 16:36:45 -070032import static com.android.documentsui.base.LocalPreferences.PERMISSION_ASK_AGAIN;
33import static com.android.documentsui.base.LocalPreferences.PERMISSION_NEVER_ASK;
34import static com.android.documentsui.base.LocalPreferences.getScopedAccessPermissionStatus;
35import static com.android.documentsui.base.LocalPreferences.setScopedAccessPermissionStatus;
36import static com.android.documentsui.base.Shared.DEBUG;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080037
Felipe Leme0bdb7c32016-03-09 17:40:49 -080038import android.annotation.SuppressLint;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080039import android.app.Activity;
Felipe Leme8cf6ce22016-02-17 17:10:45 -080040import android.app.ActivityManager;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080041import android.app.AlertDialog;
42import android.app.Dialog;
43import android.app.DialogFragment;
44import android.app.FragmentManager;
45import android.app.FragmentTransaction;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080046import android.content.ContentProviderClient;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080047import android.content.Context;
48import android.content.DialogInterface;
49import android.content.DialogInterface.OnClickListener;
50import android.content.Intent;
Felipe Leme8cf6ce22016-02-17 17:10:45 -080051import android.content.UriPermission;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080052import android.content.pm.PackageManager;
53import android.content.pm.PackageManager.NameNotFoundException;
54import android.net.Uri;
55import android.os.Bundle;
Felipe Leme5228bd02016-02-17 10:12:04 -080056import android.os.Parcelable;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080057import android.os.RemoteException;
58import android.os.UserHandle;
59import android.os.storage.StorageManager;
Felipe Leme5228bd02016-02-17 10:12:04 -080060import android.os.storage.StorageVolume;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080061import android.os.storage.VolumeInfo;
62import android.provider.DocumentsContract;
63import android.text.TextUtils;
64import android.util.Log;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080065import android.view.View;
66import android.widget.CheckBox;
67import android.widget.CompoundButton;
68import android.widget.CompoundButton.OnCheckedChangeListener;
69import android.widget.TextView;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080070
Ben Kwaf4b0ff62016-02-02 12:11:10 -080071import java.io.File;
72import java.io.IOException;
73import java.util.List;
74
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080075/**
76 * Activity responsible for handling {@link Intent#ACTION_OPEN_EXTERNAL_DOCUMENT}.
77 */
78public class OpenExternalDirectoryActivity extends Activity {
Ben Kwaf4b0ff62016-02-02 12:11:10 -080079 private static final String TAG = "OpenExternalDirectory";
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080080 private static final String FM_TAG = "open_external_directory";
81 private static final String EXTERNAL_STORAGE_AUTH = "com.android.externalstorage.documents";
82 private static final String EXTRA_FILE = "com.android.documentsui.FILE";
83 private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
84 private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
Felipe Leme0bdb7c32016-03-09 17:40:49 -080085 private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID";
Felipe Lemeb59a8a62016-03-17 18:56:20 -070086 private static final String EXTRA_IS_ROOT = "com.android.documentsui.IS_ROOT";
Felipe Leme8216cea2016-03-29 19:06:02 -070087 private static final String EXTRA_IS_PRIMARY = "com.android.documentsui.IS_PRIMARY";
Felipe Lemeb59a8a62016-03-17 18:56:20 -070088 // Special directory name representing the full volume
89 static final String DIRECTORY_ROOT = "ROOT_DIRECTORY";
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080090
Felipe Leme8cf6ce22016-02-17 17:10:45 -080091 private ContentProviderClient mExternalStorageClient;
92
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080093 @Override
94 public void onCreate(Bundle savedInstanceState) {
95 super.onCreate(savedInstanceState);
Felipe Leme0bdb7c32016-03-09 17:40:49 -080096 if (savedInstanceState != null) {
97 if (DEBUG) Log.d(TAG, "activity.onCreateDialog(): reusing instance");
98 return;
99 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800100
101 final Intent intent = getIntent();
Felipe Leme5228bd02016-02-17 10:12:04 -0800102 if (intent == null) {
103 if (DEBUG) Log.d(TAG, "missing intent");
Felipe Lemee4b60122016-02-24 10:17:41 -0800104 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme5228bd02016-02-17 10:12:04 -0800105 setResult(RESULT_CANCELED);
106 finish();
107 return;
108 }
109 final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
110 if (!(storageVolume instanceof StorageVolume)) {
111 if (DEBUG)
112 Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
113 + storageVolume);
Felipe Lemee4b60122016-02-24 10:17:41 -0800114 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme5228bd02016-02-17 10:12:04 -0800115 setResult(RESULT_CANCELED);
116 finish();
117 return;
118 }
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700119 String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME );
Felipe Leme5228bd02016-02-17 10:12:04 -0800120 if (directoryName == null) {
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700121 directoryName = DIRECTORY_ROOT;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800122 }
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800123 final StorageVolume volume = (StorageVolume) storageVolume;
124 if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(),
125 volume.getUuid(), directoryName) == PERMISSION_NEVER_ASK) {
126 logValidScopedAccessRequest(this, directoryName,
127 SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED);
128 setResult(RESULT_CANCELED);
129 finish();
130 return;
131 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800132
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800133 final int userId = UserHandle.myUserId();
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800134 if (!showFragment(this, userId, volume, directoryName)) {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800135 setResult(RESULT_CANCELED);
136 finish();
137 return;
138 }
139 }
140
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800141 @Override
142 public void onDestroy() {
143 super.onDestroy();
144 if (mExternalStorageClient != null) {
145 mExternalStorageClient.close();
146 }
147 }
148
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800149 /**
Felipe Leme5228bd02016-02-17 10:12:04 -0800150 * Validates the given path (volume + directory) and display the appropriate dialog asking the
151 * user to grant access to it.
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800152 */
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800153 private static boolean showFragment(OpenExternalDirectoryActivity activity, int userId,
154 StorageVolume storageVolume, String directoryName) {
Felipe Leme5228bd02016-02-17 10:12:04 -0800155 if (DEBUG)
156 Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
157 + directoryName + ", and user " + userId);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700158 final boolean isRoot = directoryName.equals(DIRECTORY_ROOT);
Felipe Leme8216cea2016-03-29 19:06:02 -0700159 final boolean isPrimary = storageVolume.isPrimary();
160
161 if (isRoot && isPrimary) {
162 if (DEBUG) Log.d(TAG, "root access requested on primary volume");
163 return false;
164 }
165
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700166 final File volumeRoot = storageVolume.getPathFile();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800167 File file;
168 try {
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700169 file = isRoot ? volumeRoot : new File(volumeRoot, directoryName).getCanonicalFile();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800170 } catch (IOException e) {
Felipe Leme5228bd02016-02-17 10:12:04 -0800171 Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
172 + " and directory " + directoryName);
Felipe Lemee4b60122016-02-24 10:17:41 -0800173 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800174 return false;
175 }
176 final StorageManager sm =
177 (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
178
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700179 final String root, directory;
180 if (isRoot) {
181 root = volumeRoot.getAbsolutePath();
182 directory = ".";
183 } else {
184 root = file.getParent();
185 directory = file.getName();
186 // Verify directory is valid.
187 if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
188 if (DEBUG)
189 Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
190 + file.getAbsolutePath() + "')");
191 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY);
192 return false;
193 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800194 }
195
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800196 // Gets volume label and converted path.
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800197 String volumeLabel = null;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800198 String volumeUuid = null;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800199 final List<VolumeInfo> volumes = sm.getVolumes();
200 if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size());
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700201 File internalRoot = null;
Felipe Leme3ee56ca2016-06-09 15:25:00 -0700202 boolean found = true;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800203 for (VolumeInfo volume : volumes) {
204 if (isRightVolume(volume, root, userId)) {
Felipe Leme3ee56ca2016-06-09 15:25:00 -0700205 found = true;
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700206 internalRoot = volume.getInternalPathForUser(userId);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800207 // Must convert path before calling getDocIdForFileCreateNewDir()
208 if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700209 file = isRoot ? internalRoot : new File(internalRoot, directory);
Felipe Leme3ee56ca2016-06-09 15:25:00 -0700210 volumeUuid = storageVolume.getUuid();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800211 volumeLabel = sm.getBestVolumeDescription(volume);
Felipe Leme3ee56ca2016-06-09 15:25:00 -0700212 if (TextUtils.isEmpty(volumeLabel)) {
213 volumeLabel = storageVolume.getDescription(activity);
214 }
215 if (TextUtils.isEmpty(volumeLabel)) {
216 volumeLabel = activity.getString(android.R.string.unknownName);
217 Log.w(TAG, "No volume description for " + volume + "; using " + volumeLabel);
218 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800219 break;
220 }
221 }
Felipe Lemee71de812016-04-18 14:55:15 -0700222 if (internalRoot == null) {
223 // Should not happen on normal circumstances, unless app crafted an invalid volume
224 // using reflection or the list of mounted volumes changed.
225 Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes);
226 return false;
227 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800228
229 // Checks if the user has granted the permission already.
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700230 final Intent intent = getIntentForExistingPermission(activity, isRoot, internalRoot, file);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800231 if (intent != null) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800232 logValidScopedAccessRequest(activity, directory,
233 SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800234 activity.setResult(RESULT_OK, intent);
235 activity.finish();
236 return true;
237 }
238
Felipe Leme3ee56ca2016-06-09 15:25:00 -0700239 if (!found) {
Felipe Leme5228bd02016-02-17 10:12:04 -0800240 Log.e(TAG, "Could not get volume for " + file);
Felipe Lemee4b60122016-02-24 10:17:41 -0800241 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800242 return false;
243 }
244
245 // Gets the package label.
246 final String appLabel = getAppLabel(activity);
247 if (appLabel == null) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800248 // Error already logged.
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800249 return false;
250 }
251
252 // Sets args that will be retrieve on onCreate()
253 final Bundle args = new Bundle();
254 args.putString(EXTRA_FILE, file.getAbsolutePath());
255 args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800256 args.putString(EXTRA_VOLUME_UUID, volumeUuid);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800257 args.putString(EXTRA_APP_LABEL, appLabel);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700258 args.putBoolean(EXTRA_IS_ROOT, isRoot);
Felipe Leme8216cea2016-03-29 19:06:02 -0700259 args.putBoolean(EXTRA_IS_PRIMARY, isPrimary);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800260
261 final FragmentManager fm = activity.getFragmentManager();
262 final FragmentTransaction ft = fm.beginTransaction();
263 final OpenExternalDirectoryDialogFragment fragment =
264 new OpenExternalDirectoryDialogFragment();
265 fragment.setArguments(args);
266 ft.add(fragment, FM_TAG);
267 ft.commitAllowingStateLoss();
268
269 return true;
270 }
271
272 private static String getAppLabel(Activity activity) {
273 final String packageName = activity.getCallingPackage();
274 final PackageManager pm = activity.getPackageManager();
275 try {
276 return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
277 } catch (NameNotFoundException e) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800278 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800279 Log.w(TAG, "Could not get label for package " + packageName);
280 return null;
281 }
282 }
283
284 private static boolean isRightVolume(VolumeInfo volume, String root, int userId) {
285 final File userPath = volume.getPathForUser(userId);
286 final String path = userPath == null ? null : volume.getPathForUser(userId).getPath();
Felipe Lemefffd8fc2016-06-10 11:30:56 -0700287 final boolean isMounted = volume.isMountedReadable();
Felipe Leme5228bd02016-02-17 10:12:04 -0800288 if (DEBUG)
Felipe Lemefffd8fc2016-06-10 11:30:56 -0700289 Log.d(TAG, "Volume: " + volume
290 + "\n\tuserId: " + userId
291 + "\n\tuserPath: " + userPath
292 + "\n\troot: " + root
293 + "\n\tpath: " + path
294 + "\n\tisMounted: " + isMounted);
Felipe Leme5228bd02016-02-17 10:12:04 -0800295
Felipe Lemefffd8fc2016-06-10 11:30:56 -0700296 return isMounted && root.equals(path);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800297 }
298
Felipe Lemee4b60122016-02-24 10:17:41 -0800299 private static Uri getGrantedUriPermission(Context context, ContentProviderClient provider,
300 File file) {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800301 // Calls ExternalStorageProvider to get the doc id for the file
302 final Bundle bundle;
303 try {
304 bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null);
305 } catch (RemoteException e) {
306 Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e);
Felipe Lemee4b60122016-02-24 10:17:41 -0800307 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800308 return null;
309 }
310 final String docId = bundle == null ? null : bundle.getString("DOC_ID");
311 if (docId == null) {
312 Log.e(TAG, "Did not get doc id from External Storage provider for " + file);
Felipe Lemee4b60122016-02-24 10:17:41 -0800313 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800314 return null;
315 }
Ben Kwa543a2922016-03-22 11:11:46 -0700316 if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800317
318 final Uri uri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_AUTH, docId);
319 if (uri == null) {
320 Log.e(TAG, "Could not get URI for doc id " + docId);
321 return null;
322 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800323 if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800324 return uri;
325 }
326
Felipe Lemee4b60122016-02-24 10:17:41 -0800327 private static Intent createGrantedUriPermissionsIntent(Context context,
328 ContentProviderClient provider, File file) {
329 final Uri uri = getGrantedUriPermission(context, provider, file);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800330 return createGrantedUriPermissionsIntent(uri);
331 }
332
333 private static Intent createGrantedUriPermissionsIntent(Uri uri) {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800334 final Intent intent = new Intent();
335 intent.setData(uri);
336 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
337 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
338 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
339 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
340 return intent;
341 }
342
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800343 private static Intent getIntentForExistingPermission(OpenExternalDirectoryActivity activity,
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700344 boolean isRoot, File root, File file) {
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800345 final String packageName = activity.getCallingPackage();
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700346 final ContentProviderClient storageClient = activity.getExternalStorageClient();
347 final Uri grantedUri = getGrantedUriPermission(activity, storageClient, file);
348 final Uri rootUri = root.equals(file) ? grantedUri
349 : getGrantedUriPermission(activity, storageClient, root);
350
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800351 if (DEBUG)
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700352 Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri
353 + " or its root (" + rootUri + ")");
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800354 final ActivityManager am =
355 (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
356 for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
357 final Uri uri = uriPermission.getUri();
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700358 if (uri == null) {
359 Log.w(TAG, "null URI for " + uriPermission);
360 continue;
361 }
362 if (uri.equals(grantedUri) || uri.equals(rootUri)) {
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800363 if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700364 return createGrantedUriPermissionsIntent(grantedUri);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800365 }
366 }
367 if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
368 return null;
369 }
370
Ben Kwaf4b0ff62016-02-02 12:11:10 -0800371 public static class OpenExternalDirectoryDialogFragment extends DialogFragment {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800372
373 private File mFile;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800374 private String mVolumeUuid;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800375 private String mVolumeLabel;
376 private String mAppLabel;
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700377 private boolean mIsRoot;
Felipe Leme8216cea2016-03-29 19:06:02 -0700378 private boolean mIsPrimary;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800379 private CheckBox mDontAskAgain;
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800380 private OpenExternalDirectoryActivity mActivity;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800381 private AlertDialog mDialog;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800382
383 @Override
384 public void onCreate(Bundle savedInstanceState) {
385 super.onCreate(savedInstanceState);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800386 setRetainInstance(true);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800387 final Bundle args = getArguments();
388 if (args != null) {
389 mFile = new File(args.getString(EXTRA_FILE));
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800390 mVolumeUuid = args.getString(EXTRA_VOLUME_UUID);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800391 mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
392 mAppLabel = args.getString(EXTRA_APP_LABEL);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700393 mIsRoot = args.getBoolean(EXTRA_IS_ROOT);
Felipe Leme8216cea2016-03-29 19:06:02 -0700394 mIsPrimary= args.getBoolean(EXTRA_IS_PRIMARY);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800395 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800396 mActivity = (OpenExternalDirectoryActivity) getActivity();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800397 }
398
399 @Override
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800400 public void onDestroyView() {
401 // Workaround for https://code.google.com/p/android/issues/detail?id=17423
402 if (mDialog != null && getRetainInstance()) {
403 mDialog.setDismissMessage(null);
404 }
405 super.onDestroyView();
406 }
407
408 @Override
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800409 public Dialog onCreateDialog(Bundle savedInstanceState) {
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800410 if (mDialog != null) {
411 if (DEBUG) Log.d(TAG, "fragment.onCreateDialog(): reusing dialog");
412 return mDialog;
413 }
414 if (mActivity != getActivity()) {
415 // Sanity check.
416 Log.wtf(TAG, "activity references don't match on onCreateDialog(): mActivity = "
417 + mActivity + " , getActivity() = " + getActivity());
418 mActivity = (OpenExternalDirectoryActivity) getActivity();
419 }
Felipe Lemee4b60122016-02-24 10:17:41 -0800420 final String directory = mFile.getName();
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700421 final String directoryName = mIsRoot ? DIRECTORY_ROOT : directory;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800422 final Context context = mActivity.getApplicationContext();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800423 final OnClickListener listener = new OnClickListener() {
424
425 @Override
426 public void onClick(DialogInterface dialog, int which) {
427 Intent intent = null;
428 if (which == DialogInterface.BUTTON_POSITIVE) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800429 intent = createGrantedUriPermissionsIntent(mActivity,
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800430 mActivity.getExternalStorageClient(), mFile);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800431 }
432 if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700433 logValidScopedAccessRequest(mActivity, directoryName,
Felipe Lemee4b60122016-02-24 10:17:41 -0800434 SCOPED_DIRECTORY_ACCESS_DENIED);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800435 final boolean checked = mDontAskAgain.isChecked();
436 if (checked) {
437 logValidScopedAccessRequest(mActivity, directory,
438 SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST);
439 setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700440 mVolumeUuid, directoryName, PERMISSION_NEVER_ASK);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800441 } else {
442 setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700443 mVolumeUuid, directoryName, PERMISSION_ASK_AGAIN);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800444 }
445 mActivity.setResult(RESULT_CANCELED);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800446 } else {
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800447 logValidScopedAccessRequest(mActivity, directory,
Felipe Lemee4b60122016-02-24 10:17:41 -0800448 SCOPED_DIRECTORY_ACCESS_GRANTED);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800449 mActivity.setResult(RESULT_OK, intent);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800450 }
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800451 mActivity.finish();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800452 }
453 };
454
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800455 @SuppressLint("InflateParams")
456 // It's ok pass null ViewRoot on AlertDialogs.
457 final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700458 final CharSequence message;
459 if (mIsRoot) {
460 message = TextUtils.expandTemplate(getText(
461 R.string.open_external_dialog_root_request), mAppLabel, mVolumeLabel);
462 } else {
Felipe Leme8216cea2016-03-29 19:06:02 -0700463 message = TextUtils.expandTemplate(
464 getText(mIsPrimary ? R.string.open_external_dialog_request_primary_volume
465 : R.string.open_external_dialog_request),
Felipe Leme3ee56ca2016-06-09 15:25:00 -0700466 mAppLabel, directory, mVolumeLabel);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700467 }
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800468 final TextView messageField = (TextView) view.findViewById(R.id.message);
469 messageField.setText(message);
Felipe Lemecb5e3e72016-03-15 16:29:17 -0700470 mDialog = new AlertDialog.Builder(mActivity, R.style.Theme_AppCompat_Light_Dialog_Alert)
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800471 .setView(view)
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800472 .setPositiveButton(R.string.allow, listener)
473 .setNegativeButton(R.string.deny, listener)
474 .create();
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800475
476 mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox);
477 if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700478 mVolumeUuid, directoryName) == PERMISSION_ASK_AGAIN) {
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800479 mDontAskAgain.setVisibility(View.VISIBLE);
480 mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() {
481
482 @Override
483 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
484 mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked);
485 }
486 });
487 }
488
489 return mDialog;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800490 }
491
492 @Override
493 public void onCancel(DialogInterface dialog) {
494 super.onCancel(dialog);
495 final Activity activity = getActivity();
Felipe Lemee4b60122016-02-24 10:17:41 -0800496 logValidScopedAccessRequest(activity, mFile.getName(), SCOPED_DIRECTORY_ACCESS_DENIED);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800497 activity.setResult(RESULT_CANCELED);
498 activity.finish();
499 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800500 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800501
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800502 private synchronized ContentProviderClient getExternalStorageClient() {
503 if (mExternalStorageClient == null) {
504 mExternalStorageClient =
505 getContentResolver().acquireContentProviderClient(EXTERNAL_STORAGE_AUTH);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800506 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800507 return mExternalStorageClient;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800508 }
509}