blob: b0e5e4e531e1e8cf4f28ea7bb77a97f6efd395f1 [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 Lemee4b60122016-02-24 10:17:41 -080020import static android.os.Environment.STANDARD_DIRECTORIES;
Felipe Leme5228bd02016-02-17 10:12:04 -080021import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
22import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080023import static com.android.documentsui.LocalPreferences.getScopedAccessPermissionStatus;
24import static com.android.documentsui.LocalPreferences.PERMISSION_ASK;
25import static com.android.documentsui.LocalPreferences.PERMISSION_ASK_AGAIN;
26import static com.android.documentsui.LocalPreferences.PERMISSION_NEVER_ASK;
27import static com.android.documentsui.LocalPreferences.setScopedAccessPermissionStatus;
28import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED;
Felipe Lemee4b60122016-02-24 10:17:41 -080029import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED;
30import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080031import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST;
Felipe Lemee4b60122016-02-24 10:17:41 -080032import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ERROR;
33import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_GRANTED;
34import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS;
35import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080036import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest;
37import static com.android.documentsui.Metrics.logValidScopedAccessRequest;
38import static com.android.documentsui.Shared.DEBUG;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080039
Felipe Leme0bdb7c32016-03-09 17:40:49 -080040import android.annotation.SuppressLint;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080041import android.app.Activity;
Felipe Leme8cf6ce22016-02-17 17:10:45 -080042import android.app.ActivityManager;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080043import android.app.AlertDialog;
44import android.app.Dialog;
45import android.app.DialogFragment;
46import android.app.FragmentManager;
47import android.app.FragmentTransaction;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080048import android.content.ContentProviderClient;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080049import android.content.Context;
50import android.content.DialogInterface;
51import android.content.DialogInterface.OnClickListener;
52import android.content.Intent;
Felipe Leme8cf6ce22016-02-17 17:10:45 -080053import android.content.UriPermission;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080054import android.content.pm.PackageManager;
55import android.content.pm.PackageManager.NameNotFoundException;
56import android.net.Uri;
57import android.os.Bundle;
Felipe Leme5228bd02016-02-17 10:12:04 -080058import android.os.Parcelable;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080059import android.os.RemoteException;
60import android.os.UserHandle;
61import android.os.storage.StorageManager;
Felipe Leme5228bd02016-02-17 10:12:04 -080062import android.os.storage.StorageVolume;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080063import android.os.storage.VolumeInfo;
64import android.provider.DocumentsContract;
65import android.text.TextUtils;
66import android.util.Log;
Felipe Leme0bdb7c32016-03-09 17:40:49 -080067import android.view.View;
68import android.widget.CheckBox;
69import android.widget.CompoundButton;
70import android.widget.CompoundButton.OnCheckedChangeListener;
71import android.widget.TextView;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080072
Ben Kwaf4b0ff62016-02-02 12:11:10 -080073import java.io.File;
74import java.io.IOException;
75import java.util.List;
76
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080077/**
78 * Activity responsible for handling {@link Intent#ACTION_OPEN_EXTERNAL_DOCUMENT}.
79 */
80public class OpenExternalDirectoryActivity extends Activity {
Ben Kwaf4b0ff62016-02-02 12:11:10 -080081 private static final String TAG = "OpenExternalDirectory";
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080082 private static final String FM_TAG = "open_external_directory";
83 private static final String EXTERNAL_STORAGE_AUTH = "com.android.externalstorage.documents";
84 private static final String EXTRA_FILE = "com.android.documentsui.FILE";
85 private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
86 private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
Felipe Leme0bdb7c32016-03-09 17:40:49 -080087 private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID";
Felipe Lemeb59a8a62016-03-17 18:56:20 -070088 private static final String EXTRA_IS_ROOT = "com.android.documentsui.IS_ROOT";
Felipe Leme8216cea2016-03-29 19:06:02 -070089 private static final String EXTRA_IS_PRIMARY = "com.android.documentsui.IS_PRIMARY";
Felipe Lemeb59a8a62016-03-17 18:56:20 -070090 // Special directory name representing the full volume
91 static final String DIRECTORY_ROOT = "ROOT_DIRECTORY";
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080092
Felipe Leme8cf6ce22016-02-17 17:10:45 -080093 private ContentProviderClient mExternalStorageClient;
94
Felipe Lemea4f4d7e2016-01-22 16:49:55 -080095 @Override
96 public void onCreate(Bundle savedInstanceState) {
97 super.onCreate(savedInstanceState);
Felipe Leme0bdb7c32016-03-09 17:40:49 -080098 if (savedInstanceState != null) {
99 if (DEBUG) Log.d(TAG, "activity.onCreateDialog(): reusing instance");
100 return;
101 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800102
103 final Intent intent = getIntent();
Felipe Leme5228bd02016-02-17 10:12:04 -0800104 if (intent == null) {
105 if (DEBUG) Log.d(TAG, "missing intent");
Felipe Lemee4b60122016-02-24 10:17:41 -0800106 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme5228bd02016-02-17 10:12:04 -0800107 setResult(RESULT_CANCELED);
108 finish();
109 return;
110 }
111 final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
112 if (!(storageVolume instanceof StorageVolume)) {
113 if (DEBUG)
114 Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
115 + storageVolume);
Felipe Lemee4b60122016-02-24 10:17:41 -0800116 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme5228bd02016-02-17 10:12:04 -0800117 setResult(RESULT_CANCELED);
118 finish();
119 return;
120 }
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700121 String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME );
Felipe Leme5228bd02016-02-17 10:12:04 -0800122 if (directoryName == null) {
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700123 directoryName = DIRECTORY_ROOT;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800124 }
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800125 final StorageVolume volume = (StorageVolume) storageVolume;
126 if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(),
127 volume.getUuid(), directoryName) == PERMISSION_NEVER_ASK) {
128 logValidScopedAccessRequest(this, directoryName,
129 SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED);
130 setResult(RESULT_CANCELED);
131 finish();
132 return;
133 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800134
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800135 final int userId = UserHandle.myUserId();
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800136 if (!showFragment(this, userId, volume, directoryName)) {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800137 setResult(RESULT_CANCELED);
138 finish();
139 return;
140 }
141 }
142
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800143 @Override
144 public void onDestroy() {
145 super.onDestroy();
146 if (mExternalStorageClient != null) {
147 mExternalStorageClient.close();
148 }
149 }
150
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800151 /**
Felipe Leme5228bd02016-02-17 10:12:04 -0800152 * Validates the given path (volume + directory) and display the appropriate dialog asking the
153 * user to grant access to it.
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800154 */
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800155 private static boolean showFragment(OpenExternalDirectoryActivity activity, int userId,
156 StorageVolume storageVolume, String directoryName) {
Felipe Leme5228bd02016-02-17 10:12:04 -0800157 if (DEBUG)
158 Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
159 + directoryName + ", and user " + userId);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700160 final boolean isRoot = directoryName.equals(DIRECTORY_ROOT);
Felipe Leme8216cea2016-03-29 19:06:02 -0700161 final boolean isPrimary = storageVolume.isPrimary();
162
163 if (isRoot && isPrimary) {
164 if (DEBUG) Log.d(TAG, "root access requested on primary volume");
165 return false;
166 }
167
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700168 final File volumeRoot = storageVolume.getPathFile();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800169 File file;
170 try {
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700171 file = isRoot ? volumeRoot : new File(volumeRoot, directoryName).getCanonicalFile();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800172 } catch (IOException e) {
Felipe Leme5228bd02016-02-17 10:12:04 -0800173 Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
174 + " and directory " + directoryName);
Felipe Lemee4b60122016-02-24 10:17:41 -0800175 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800176 return false;
177 }
178 final StorageManager sm =
179 (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
180
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700181 final String root, directory;
182 if (isRoot) {
183 root = volumeRoot.getAbsolutePath();
184 directory = ".";
185 } else {
186 root = file.getParent();
187 directory = file.getName();
188 // Verify directory is valid.
189 if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
190 if (DEBUG)
191 Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
192 + file.getAbsolutePath() + "')");
193 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY);
194 return false;
195 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800196 }
197
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800198 // Gets volume label and converted path.
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800199 String volumeLabel = null;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800200 String volumeUuid = null;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800201 final List<VolumeInfo> volumes = sm.getVolumes();
202 if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size());
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700203 File internalRoot = null;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800204 for (VolumeInfo volume : volumes) {
205 if (isRightVolume(volume, root, userId)) {
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 Lemea4f4d7e2016-01-22 16:49:55 -0800210 volumeLabel = sm.getBestVolumeDescription(volume);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800211 volumeUuid = volume.getFsUuid();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800212 break;
213 }
214 }
Felipe Lemee71de812016-04-18 14:55:15 -0700215 if (internalRoot == null) {
216 // Should not happen on normal circumstances, unless app crafted an invalid volume
217 // using reflection or the list of mounted volumes changed.
218 Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes);
219 return false;
220 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800221
222 // Checks if the user has granted the permission already.
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700223 final Intent intent = getIntentForExistingPermission(activity, isRoot, internalRoot, file);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800224 if (intent != null) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800225 logValidScopedAccessRequest(activity, directory,
226 SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800227 activity.setResult(RESULT_OK, intent);
228 activity.finish();
229 return true;
230 }
231
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800232 if (volumeLabel == null) {
Felipe Leme5228bd02016-02-17 10:12:04 -0800233 Log.e(TAG, "Could not get volume for " + file);
Felipe Lemee4b60122016-02-24 10:17:41 -0800234 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800235 return false;
236 }
237
238 // Gets the package label.
239 final String appLabel = getAppLabel(activity);
240 if (appLabel == null) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800241 // Error already logged.
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800242 return false;
243 }
244
245 // Sets args that will be retrieve on onCreate()
246 final Bundle args = new Bundle();
247 args.putString(EXTRA_FILE, file.getAbsolutePath());
248 args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800249 args.putString(EXTRA_VOLUME_UUID, volumeUuid);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800250 args.putString(EXTRA_APP_LABEL, appLabel);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700251 args.putBoolean(EXTRA_IS_ROOT, isRoot);
Felipe Leme8216cea2016-03-29 19:06:02 -0700252 args.putBoolean(EXTRA_IS_PRIMARY, isPrimary);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800253
254 final FragmentManager fm = activity.getFragmentManager();
255 final FragmentTransaction ft = fm.beginTransaction();
256 final OpenExternalDirectoryDialogFragment fragment =
257 new OpenExternalDirectoryDialogFragment();
258 fragment.setArguments(args);
259 ft.add(fragment, FM_TAG);
260 ft.commitAllowingStateLoss();
261
262 return true;
263 }
264
265 private static String getAppLabel(Activity activity) {
266 final String packageName = activity.getCallingPackage();
267 final PackageManager pm = activity.getPackageManager();
268 try {
269 return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
270 } catch (NameNotFoundException e) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800271 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800272 Log.w(TAG, "Could not get label for package " + packageName);
273 return null;
274 }
275 }
276
277 private static boolean isRightVolume(VolumeInfo volume, String root, int userId) {
278 final File userPath = volume.getPathForUser(userId);
279 final String path = userPath == null ? null : volume.getPathForUser(userId).getPath();
280 final boolean isVisible = volume.isVisibleForWrite(userId);
Felipe Leme5228bd02016-02-17 10:12:04 -0800281 if (DEBUG)
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800282 Log.d(TAG, "Volume: " + volume + " userId: " + userId + " root: " + root
283 + " volumePath: " + volume.getPath().getPath()
284 + " pathForUser: " + path
285 + " internalPathForUser: " + volume.getInternalPath()
286 + " isVisible: " + isVisible);
Felipe Leme5228bd02016-02-17 10:12:04 -0800287
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800288 return volume.isVisibleForWrite(userId) && root.equals(path);
289 }
290
Felipe Lemee4b60122016-02-24 10:17:41 -0800291 private static Uri getGrantedUriPermission(Context context, ContentProviderClient provider,
292 File file) {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800293 // Calls ExternalStorageProvider to get the doc id for the file
294 final Bundle bundle;
295 try {
296 bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null);
297 } catch (RemoteException e) {
298 Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e);
Felipe Lemee4b60122016-02-24 10:17:41 -0800299 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800300 return null;
301 }
302 final String docId = bundle == null ? null : bundle.getString("DOC_ID");
303 if (docId == null) {
304 Log.e(TAG, "Did not get doc id from External Storage provider for " + file);
Felipe Lemee4b60122016-02-24 10:17:41 -0800305 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800306 return null;
307 }
Ben Kwa543a2922016-03-22 11:11:46 -0700308 if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800309
310 final Uri uri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_AUTH, docId);
311 if (uri == null) {
312 Log.e(TAG, "Could not get URI for doc id " + docId);
313 return null;
314 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800315 if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800316 return uri;
317 }
318
Felipe Lemee4b60122016-02-24 10:17:41 -0800319 private static Intent createGrantedUriPermissionsIntent(Context context,
320 ContentProviderClient provider, File file) {
321 final Uri uri = getGrantedUriPermission(context, provider, file);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800322 return createGrantedUriPermissionsIntent(uri);
323 }
324
325 private static Intent createGrantedUriPermissionsIntent(Uri uri) {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800326 final Intent intent = new Intent();
327 intent.setData(uri);
328 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
329 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
330 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
331 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
332 return intent;
333 }
334
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800335 private static Intent getIntentForExistingPermission(OpenExternalDirectoryActivity activity,
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700336 boolean isRoot, File root, File file) {
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800337 final String packageName = activity.getCallingPackage();
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700338 final ContentProviderClient storageClient = activity.getExternalStorageClient();
339 final Uri grantedUri = getGrantedUriPermission(activity, storageClient, file);
340 final Uri rootUri = root.equals(file) ? grantedUri
341 : getGrantedUriPermission(activity, storageClient, root);
342
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800343 if (DEBUG)
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700344 Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri
345 + " or its root (" + rootUri + ")");
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800346 final ActivityManager am =
347 (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
348 for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
349 final Uri uri = uriPermission.getUri();
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700350 if (uri == null) {
351 Log.w(TAG, "null URI for " + uriPermission);
352 continue;
353 }
354 if (uri.equals(grantedUri) || uri.equals(rootUri)) {
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800355 if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700356 return createGrantedUriPermissionsIntent(grantedUri);
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800357 }
358 }
359 if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
360 return null;
361 }
362
Ben Kwaf4b0ff62016-02-02 12:11:10 -0800363 public static class OpenExternalDirectoryDialogFragment extends DialogFragment {
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800364
365 private File mFile;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800366 private String mVolumeUuid;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800367 private String mVolumeLabel;
368 private String mAppLabel;
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700369 private boolean mIsRoot;
Felipe Leme8216cea2016-03-29 19:06:02 -0700370 private boolean mIsPrimary;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800371 private CheckBox mDontAskAgain;
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800372 private OpenExternalDirectoryActivity mActivity;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800373 private AlertDialog mDialog;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800374
375 @Override
376 public void onCreate(Bundle savedInstanceState) {
377 super.onCreate(savedInstanceState);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800378 setRetainInstance(true);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800379 final Bundle args = getArguments();
380 if (args != null) {
381 mFile = new File(args.getString(EXTRA_FILE));
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800382 mVolumeUuid = args.getString(EXTRA_VOLUME_UUID);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800383 mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
384 mAppLabel = args.getString(EXTRA_APP_LABEL);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700385 mIsRoot = args.getBoolean(EXTRA_IS_ROOT);
Felipe Leme8216cea2016-03-29 19:06:02 -0700386 mIsPrimary= args.getBoolean(EXTRA_IS_PRIMARY);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800387 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800388 mActivity = (OpenExternalDirectoryActivity) getActivity();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800389 }
390
391 @Override
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800392 public void onDestroyView() {
393 // Workaround for https://code.google.com/p/android/issues/detail?id=17423
394 if (mDialog != null && getRetainInstance()) {
395 mDialog.setDismissMessage(null);
396 }
397 super.onDestroyView();
398 }
399
400 @Override
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800401 public Dialog onCreateDialog(Bundle savedInstanceState) {
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800402 if (mDialog != null) {
403 if (DEBUG) Log.d(TAG, "fragment.onCreateDialog(): reusing dialog");
404 return mDialog;
405 }
406 if (mActivity != getActivity()) {
407 // Sanity check.
408 Log.wtf(TAG, "activity references don't match on onCreateDialog(): mActivity = "
409 + mActivity + " , getActivity() = " + getActivity());
410 mActivity = (OpenExternalDirectoryActivity) getActivity();
411 }
Felipe Lemee4b60122016-02-24 10:17:41 -0800412 final String directory = mFile.getName();
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700413 final String directoryName = mIsRoot ? DIRECTORY_ROOT : directory;
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800414 final Context context = mActivity.getApplicationContext();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800415 final OnClickListener listener = new OnClickListener() {
416
417 @Override
418 public void onClick(DialogInterface dialog, int which) {
419 Intent intent = null;
420 if (which == DialogInterface.BUTTON_POSITIVE) {
Felipe Lemee4b60122016-02-24 10:17:41 -0800421 intent = createGrantedUriPermissionsIntent(mActivity,
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800422 mActivity.getExternalStorageClient(), mFile);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800423 }
424 if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700425 logValidScopedAccessRequest(mActivity, directoryName,
Felipe Lemee4b60122016-02-24 10:17:41 -0800426 SCOPED_DIRECTORY_ACCESS_DENIED);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800427 final boolean checked = mDontAskAgain.isChecked();
428 if (checked) {
429 logValidScopedAccessRequest(mActivity, directory,
430 SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST);
431 setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700432 mVolumeUuid, directoryName, PERMISSION_NEVER_ASK);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800433 } else {
434 setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700435 mVolumeUuid, directoryName, PERMISSION_ASK_AGAIN);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800436 }
437 mActivity.setResult(RESULT_CANCELED);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800438 } else {
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800439 logValidScopedAccessRequest(mActivity, directory,
Felipe Lemee4b60122016-02-24 10:17:41 -0800440 SCOPED_DIRECTORY_ACCESS_GRANTED);
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800441 mActivity.setResult(RESULT_OK, intent);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800442 }
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800443 mActivity.finish();
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800444 }
445 };
446
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800447 @SuppressLint("InflateParams")
448 // It's ok pass null ViewRoot on AlertDialogs.
449 final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null);
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700450 final CharSequence message;
451 if (mIsRoot) {
452 message = TextUtils.expandTemplate(getText(
453 R.string.open_external_dialog_root_request), mAppLabel, mVolumeLabel);
454 } else {
Felipe Leme8216cea2016-03-29 19:06:02 -0700455 message = TextUtils.expandTemplate(
456 getText(mIsPrimary ? R.string.open_external_dialog_request_primary_volume
457 : R.string.open_external_dialog_request),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700458 mAppLabel, directory, mVolumeLabel);
459 }
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800460 final TextView messageField = (TextView) view.findViewById(R.id.message);
461 messageField.setText(message);
Felipe Lemecb5e3e72016-03-15 16:29:17 -0700462 mDialog = new AlertDialog.Builder(mActivity, R.style.Theme_AppCompat_Light_Dialog_Alert)
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800463 .setView(view)
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800464 .setPositiveButton(R.string.allow, listener)
465 .setNegativeButton(R.string.deny, listener)
466 .create();
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800467
468 mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox);
469 if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
Felipe Lemeb59a8a62016-03-17 18:56:20 -0700470 mVolumeUuid, directoryName) == PERMISSION_ASK_AGAIN) {
Felipe Leme0bdb7c32016-03-09 17:40:49 -0800471 mDontAskAgain.setVisibility(View.VISIBLE);
472 mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() {
473
474 @Override
475 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
476 mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked);
477 }
478 });
479 }
480
481 return mDialog;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800482 }
483
484 @Override
485 public void onCancel(DialogInterface dialog) {
486 super.onCancel(dialog);
487 final Activity activity = getActivity();
Felipe Lemee4b60122016-02-24 10:17:41 -0800488 logValidScopedAccessRequest(activity, mFile.getName(), SCOPED_DIRECTORY_ACCESS_DENIED);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800489 activity.setResult(RESULT_CANCELED);
490 activity.finish();
491 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800492 }
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800493
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800494 private synchronized ContentProviderClient getExternalStorageClient() {
495 if (mExternalStorageClient == null) {
496 mExternalStorageClient =
497 getContentResolver().acquireContentProviderClient(EXTERNAL_STORAGE_AUTH);
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800498 }
Felipe Leme8cf6ce22016-02-17 17:10:45 -0800499 return mExternalStorageClient;
Felipe Lemea4f4d7e2016-01-22 16:49:55 -0800500 }
501}