blob: dc529ceb18ece2afb2164a1e08c99565a7eb0da1 [file] [log] [blame]
Felipe Lemeb012f912016-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 Leme3e166b22016-02-24 10:17:41 -080020import static android.os.Environment.STANDARD_DIRECTORIES;
Felipe Leme34a9d522016-02-17 10:12:04 -080021import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
22import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
Felipe Lemeb012f912016-01-22 16:49:55 -080023import static com.android.documentsui.Shared.DEBUG;
Felipe Leme3e166b22016-02-24 10:17:41 -080024import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest;
25import static com.android.documentsui.Metrics.logValidScopedAccessRequest;
26import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED;
27import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED;
28import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ERROR;
29import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_GRANTED;
30import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS;
31import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY;
Felipe Lemeb012f912016-01-22 16:49:55 -080032
Felipe Lemeb012f912016-01-22 16:49:55 -080033import android.app.Activity;
Felipe Leme560d23a2016-02-17 17:10:45 -080034import android.app.ActivityManager;
Felipe Lemeb012f912016-01-22 16:49:55 -080035import android.app.AlertDialog;
36import android.app.Dialog;
37import android.app.DialogFragment;
38import android.app.FragmentManager;
39import android.app.FragmentTransaction;
Felipe Lemeb012f912016-01-22 16:49:55 -080040import android.content.ContentProviderClient;
41import android.content.ContentResolver;
42import android.content.Context;
43import android.content.DialogInterface;
44import android.content.DialogInterface.OnClickListener;
45import android.content.Intent;
Felipe Leme560d23a2016-02-17 17:10:45 -080046import android.content.UriPermission;
Felipe Lemeb012f912016-01-22 16:49:55 -080047import android.content.pm.PackageManager;
48import android.content.pm.PackageManager.NameNotFoundException;
49import android.net.Uri;
50import android.os.Bundle;
Felipe Leme34a9d522016-02-17 10:12:04 -080051import android.os.Parcelable;
Felipe Lemeb012f912016-01-22 16:49:55 -080052import android.os.RemoteException;
53import android.os.UserHandle;
54import android.os.storage.StorageManager;
Felipe Leme34a9d522016-02-17 10:12:04 -080055import android.os.storage.StorageVolume;
Felipe Lemeb012f912016-01-22 16:49:55 -080056import android.os.storage.VolumeInfo;
57import android.provider.DocumentsContract;
58import android.text.TextUtils;
59import android.util.Log;
60
Ben Kwae3aee182016-02-02 12:11:10 -080061import java.io.File;
62import java.io.IOException;
63import java.util.List;
64
Felipe Lemeb012f912016-01-22 16:49:55 -080065/**
66 * Activity responsible for handling {@link Intent#ACTION_OPEN_EXTERNAL_DOCUMENT}.
67 */
68public class OpenExternalDirectoryActivity extends Activity {
Ben Kwae3aee182016-02-02 12:11:10 -080069 private static final String TAG = "OpenExternalDirectory";
Felipe Lemeb012f912016-01-22 16:49:55 -080070 private static final String FM_TAG = "open_external_directory";
71 private static final String EXTERNAL_STORAGE_AUTH = "com.android.externalstorage.documents";
72 private static final String EXTRA_FILE = "com.android.documentsui.FILE";
73 private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
74 private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
75
Felipe Leme560d23a2016-02-17 17:10:45 -080076 private ContentProviderClient mExternalStorageClient;
77
Felipe Lemeb012f912016-01-22 16:49:55 -080078 @Override
79 public void onCreate(Bundle savedInstanceState) {
80 super.onCreate(savedInstanceState);
81
82 final Intent intent = getIntent();
Felipe Leme34a9d522016-02-17 10:12:04 -080083 if (intent == null) {
84 if (DEBUG) Log.d(TAG, "missing intent");
Felipe Leme3e166b22016-02-24 10:17:41 -080085 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme34a9d522016-02-17 10:12:04 -080086 setResult(RESULT_CANCELED);
87 finish();
88 return;
89 }
90 final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
91 if (!(storageVolume instanceof StorageVolume)) {
92 if (DEBUG)
93 Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
94 + storageVolume);
Felipe Leme3e166b22016-02-24 10:17:41 -080095 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme34a9d522016-02-17 10:12:04 -080096 setResult(RESULT_CANCELED);
97 finish();
98 return;
99 }
100 final String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME);
101 if (directoryName == null) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800102 logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
Felipe Leme34a9d522016-02-17 10:12:04 -0800103 if (DEBUG) Log.d(TAG, "missing extra " + EXTRA_DIRECTORY_NAME + " on " + intent);
Felipe Lemeb012f912016-01-22 16:49:55 -0800104 setResult(RESULT_CANCELED);
105 finish();
106 return;
107 }
108
Felipe Lemeb012f912016-01-22 16:49:55 -0800109 final int userId = UserHandle.myUserId();
Felipe Leme34a9d522016-02-17 10:12:04 -0800110 if (!showFragment(this, userId, (StorageVolume) storageVolume, directoryName)) {
Felipe Lemeb012f912016-01-22 16:49:55 -0800111 setResult(RESULT_CANCELED);
112 finish();
113 return;
114 }
115 }
116
Felipe Leme560d23a2016-02-17 17:10:45 -0800117 @Override
118 public void onDestroy() {
119 super.onDestroy();
120 if (mExternalStorageClient != null) {
121 mExternalStorageClient.close();
122 }
123 }
124
Felipe Lemeb012f912016-01-22 16:49:55 -0800125 /**
Felipe Leme34a9d522016-02-17 10:12:04 -0800126 * Validates the given path (volume + directory) and display the appropriate dialog asking the
127 * user to grant access to it.
Felipe Lemeb012f912016-01-22 16:49:55 -0800128 */
Felipe Leme560d23a2016-02-17 17:10:45 -0800129 private static boolean showFragment(OpenExternalDirectoryActivity activity, int userId,
130 StorageVolume storageVolume, String directoryName) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800131 if (DEBUG)
132 Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
133 + directoryName + ", and user " + userId);
Felipe Lemeb012f912016-01-22 16:49:55 -0800134 File file;
135 try {
Felipe Leme34a9d522016-02-17 10:12:04 -0800136 file = new File(storageVolume.getPathFile(), directoryName).getCanonicalFile();
Felipe Lemeb012f912016-01-22 16:49:55 -0800137 } catch (IOException e) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800138 Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
139 + " and directory " + directoryName);
Felipe Leme3e166b22016-02-24 10:17:41 -0800140 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemeb012f912016-01-22 16:49:55 -0800141 return false;
142 }
143 final StorageManager sm =
144 (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
145
146 final String root = file.getParent();
147 final String directory = file.getName();
148
149 // Verify directory is valid.
150 if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800151 if (DEBUG)
152 Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
153 + file.getAbsolutePath() + "')");
Felipe Leme3e166b22016-02-24 10:17:41 -0800154 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY);
Felipe Lemeb012f912016-01-22 16:49:55 -0800155 return false;
156 }
157
Felipe Leme560d23a2016-02-17 17:10:45 -0800158 // Gets volume label and converted path.
Felipe Lemeb012f912016-01-22 16:49:55 -0800159 String volumeLabel = null;
160 final List<VolumeInfo> volumes = sm.getVolumes();
161 if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size());
162 for (VolumeInfo volume : volumes) {
163 if (isRightVolume(volume, root, userId)) {
164 final File internalRoot = volume.getInternalPathForUser(userId);
165 // Must convert path before calling getDocIdForFileCreateNewDir()
166 if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot);
167 file = new File(internalRoot, directory);
168 volumeLabel = sm.getBestVolumeDescription(volume);
169 break;
170 }
171 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800172
173 // Checks if the user has granted the permission already.
174 final Intent intent = getIntentForExistingPermission(activity, file);
175 if (intent != null) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800176 logValidScopedAccessRequest(activity, directory,
177 SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED);
Felipe Leme560d23a2016-02-17 17:10:45 -0800178 activity.setResult(RESULT_OK, intent);
179 activity.finish();
180 return true;
181 }
182
Felipe Lemeb012f912016-01-22 16:49:55 -0800183 if (volumeLabel == null) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800184 Log.e(TAG, "Could not get volume for " + file);
Felipe Leme3e166b22016-02-24 10:17:41 -0800185 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemeb012f912016-01-22 16:49:55 -0800186 return false;
187 }
188
189 // Gets the package label.
190 final String appLabel = getAppLabel(activity);
191 if (appLabel == null) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800192 // Error already logged.
Felipe Lemeb012f912016-01-22 16:49:55 -0800193 return false;
194 }
195
196 // Sets args that will be retrieve on onCreate()
197 final Bundle args = new Bundle();
198 args.putString(EXTRA_FILE, file.getAbsolutePath());
199 args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
200 args.putString(EXTRA_APP_LABEL, appLabel);
201
202 final FragmentManager fm = activity.getFragmentManager();
203 final FragmentTransaction ft = fm.beginTransaction();
204 final OpenExternalDirectoryDialogFragment fragment =
205 new OpenExternalDirectoryDialogFragment();
206 fragment.setArguments(args);
207 ft.add(fragment, FM_TAG);
208 ft.commitAllowingStateLoss();
209
210 return true;
211 }
212
213 private static String getAppLabel(Activity activity) {
214 final String packageName = activity.getCallingPackage();
215 final PackageManager pm = activity.getPackageManager();
216 try {
217 return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
218 } catch (NameNotFoundException e) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800219 logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemeb012f912016-01-22 16:49:55 -0800220 Log.w(TAG, "Could not get label for package " + packageName);
221 return null;
222 }
223 }
224
225 private static boolean isRightVolume(VolumeInfo volume, String root, int userId) {
226 final File userPath = volume.getPathForUser(userId);
227 final String path = userPath == null ? null : volume.getPathForUser(userId).getPath();
228 final boolean isVisible = volume.isVisibleForWrite(userId);
Felipe Leme34a9d522016-02-17 10:12:04 -0800229 if (DEBUG)
Felipe Lemeb012f912016-01-22 16:49:55 -0800230 Log.d(TAG, "Volume: " + volume + " userId: " + userId + " root: " + root
231 + " volumePath: " + volume.getPath().getPath()
232 + " pathForUser: " + path
233 + " internalPathForUser: " + volume.getInternalPath()
234 + " isVisible: " + isVisible);
Felipe Leme34a9d522016-02-17 10:12:04 -0800235
Felipe Lemeb012f912016-01-22 16:49:55 -0800236 return volume.isVisibleForWrite(userId) && root.equals(path);
237 }
238
Felipe Leme3e166b22016-02-24 10:17:41 -0800239 private static Uri getGrantedUriPermission(Context context, ContentProviderClient provider,
240 File file) {
Felipe Lemeb012f912016-01-22 16:49:55 -0800241 // Calls ExternalStorageProvider to get the doc id for the file
242 final Bundle bundle;
243 try {
244 bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null);
245 } catch (RemoteException e) {
246 Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e);
Felipe Leme3e166b22016-02-24 10:17:41 -0800247 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemeb012f912016-01-22 16:49:55 -0800248 return null;
249 }
250 final String docId = bundle == null ? null : bundle.getString("DOC_ID");
251 if (docId == null) {
252 Log.e(TAG, "Did not get doc id from External Storage provider for " + file);
Felipe Leme3e166b22016-02-24 10:17:41 -0800253 logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
Felipe Lemeb012f912016-01-22 16:49:55 -0800254 return null;
255 }
256 Log.d(TAG, "doc id for " + file + ": " + docId);
257
258 final Uri uri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_AUTH, docId);
259 if (uri == null) {
260 Log.e(TAG, "Could not get URI for doc id " + docId);
261 return null;
262 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800263 if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri);
Felipe Leme560d23a2016-02-17 17:10:45 -0800264 return uri;
265 }
266
Felipe Leme3e166b22016-02-24 10:17:41 -0800267 private static Intent createGrantedUriPermissionsIntent(Context context,
268 ContentProviderClient provider, File file) {
269 final Uri uri = getGrantedUriPermission(context, provider, file);
Felipe Leme560d23a2016-02-17 17:10:45 -0800270 return createGrantedUriPermissionsIntent(uri);
271 }
272
273 private static Intent createGrantedUriPermissionsIntent(Uri uri) {
Felipe Lemeb012f912016-01-22 16:49:55 -0800274 final Intent intent = new Intent();
275 intent.setData(uri);
276 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
277 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
278 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
279 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
280 return intent;
281 }
282
Felipe Leme560d23a2016-02-17 17:10:45 -0800283 private static Intent getIntentForExistingPermission(OpenExternalDirectoryActivity activity,
284 File file) {
285 final String packageName = activity.getCallingPackage();
Felipe Leme3e166b22016-02-24 10:17:41 -0800286 final Uri grantedUri =
287 getGrantedUriPermission(activity, activity.getExternalStorageClient(), file);
Felipe Leme560d23a2016-02-17 17:10:45 -0800288 if (DEBUG)
289 Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri);
290 final ActivityManager am =
291 (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
292 for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
293 final Uri uri = uriPermission.getUri();
294 if (uri.equals(grantedUri)) {
295 if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
296 return createGrantedUriPermissionsIntent(uri);
297 }
298 }
299 if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
300 return null;
301 }
302
Ben Kwae3aee182016-02-02 12:11:10 -0800303 public static class OpenExternalDirectoryDialogFragment extends DialogFragment {
Felipe Lemeb012f912016-01-22 16:49:55 -0800304
305 private File mFile;
306 private String mVolumeLabel;
307 private String mAppLabel;
Felipe Leme560d23a2016-02-17 17:10:45 -0800308 private OpenExternalDirectoryActivity mActivity;
Felipe Lemeb012f912016-01-22 16:49:55 -0800309
310 @Override
311 public void onCreate(Bundle savedInstanceState) {
312 super.onCreate(savedInstanceState);
313 final Bundle args = getArguments();
314 if (args != null) {
315 mFile = new File(args.getString(EXTRA_FILE));
316 mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
317 mAppLabel = args.getString(EXTRA_APP_LABEL);
Felipe Lemeb012f912016-01-22 16:49:55 -0800318 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800319 mActivity = (OpenExternalDirectoryActivity) getActivity();
Felipe Lemeb012f912016-01-22 16:49:55 -0800320 }
321
322 @Override
323 public Dialog onCreateDialog(Bundle savedInstanceState) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800324 final String directory = mFile.getName();
Felipe Lemeb012f912016-01-22 16:49:55 -0800325 final Activity activity = getActivity();
326 final OnClickListener listener = new OnClickListener() {
327
328 @Override
329 public void onClick(DialogInterface dialog, int which) {
330 Intent intent = null;
331 if (which == DialogInterface.BUTTON_POSITIVE) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800332 intent = createGrantedUriPermissionsIntent(mActivity,
Felipe Leme560d23a2016-02-17 17:10:45 -0800333 mActivity.getExternalStorageClient(), mFile);
Felipe Lemeb012f912016-01-22 16:49:55 -0800334 }
335 if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
Felipe Leme3e166b22016-02-24 10:17:41 -0800336 logValidScopedAccessRequest(activity, directory,
337 SCOPED_DIRECTORY_ACCESS_DENIED);
Felipe Lemeb012f912016-01-22 16:49:55 -0800338 activity.setResult(RESULT_CANCELED);
339 } else {
Felipe Leme3e166b22016-02-24 10:17:41 -0800340 logValidScopedAccessRequest(activity, directory,
341 SCOPED_DIRECTORY_ACCESS_GRANTED);
Felipe Lemeb012f912016-01-22 16:49:55 -0800342 activity.setResult(RESULT_OK, intent);
343 }
344 activity.finish();
345 }
346 };
347
348 final CharSequence message = TextUtils
349 .expandTemplate(
Felipe Leme3e166b22016-02-24 10:17:41 -0800350 getText(R.string.open_external_dialog_request), mAppLabel, directory,
Felipe Lemeb012f912016-01-22 16:49:55 -0800351 mVolumeLabel);
352 return new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
353 .setMessage(message)
354 .setPositiveButton(R.string.allow, listener)
355 .setNegativeButton(R.string.deny, listener)
356 .create();
357 }
358
359 @Override
360 public void onCancel(DialogInterface dialog) {
361 super.onCancel(dialog);
362 final Activity activity = getActivity();
Felipe Leme3e166b22016-02-24 10:17:41 -0800363 logValidScopedAccessRequest(activity, mFile.getName(), SCOPED_DIRECTORY_ACCESS_DENIED);
Felipe Lemeb012f912016-01-22 16:49:55 -0800364 activity.setResult(RESULT_CANCELED);
365 activity.finish();
366 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800367 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800368
Felipe Leme560d23a2016-02-17 17:10:45 -0800369 private synchronized ContentProviderClient getExternalStorageClient() {
370 if (mExternalStorageClient == null) {
371 mExternalStorageClient =
372 getContentResolver().acquireContentProviderClient(EXTERNAL_STORAGE_AUTH);
Felipe Lemeb012f912016-01-22 16:49:55 -0800373 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800374 return mExternalStorageClient;
Felipe Lemeb012f912016-01-22 16:49:55 -0800375 }
376}