blob: 27d6797fdce6bf78a72e21da57986739e2f24031 [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 Leme34a9d522016-02-17 10:12:04 -080020import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
21import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
Felipe Lemeb012f912016-01-22 16:49:55 -080022import static com.android.documentsui.Shared.DEBUG;
23
Felipe Lemeb012f912016-01-22 16:49:55 -080024import android.app.Activity;
Felipe Leme560d23a2016-02-17 17:10:45 -080025import android.app.ActivityManager;
Felipe Lemeb012f912016-01-22 16:49:55 -080026import android.app.AlertDialog;
27import android.app.Dialog;
28import android.app.DialogFragment;
29import android.app.FragmentManager;
30import android.app.FragmentTransaction;
Felipe Lemeb012f912016-01-22 16:49:55 -080031import android.content.ContentProviderClient;
32import android.content.ContentResolver;
33import android.content.Context;
34import android.content.DialogInterface;
35import android.content.DialogInterface.OnClickListener;
36import android.content.Intent;
Felipe Leme560d23a2016-02-17 17:10:45 -080037import android.content.UriPermission;
Felipe Lemeb012f912016-01-22 16:49:55 -080038import android.content.pm.PackageManager;
39import android.content.pm.PackageManager.NameNotFoundException;
40import android.net.Uri;
41import android.os.Bundle;
Felipe Leme34a9d522016-02-17 10:12:04 -080042import android.os.Parcelable;
Felipe Lemeb012f912016-01-22 16:49:55 -080043import android.os.RemoteException;
44import android.os.UserHandle;
45import android.os.storage.StorageManager;
Felipe Leme34a9d522016-02-17 10:12:04 -080046import android.os.storage.StorageVolume;
Felipe Lemeb012f912016-01-22 16:49:55 -080047import android.os.storage.VolumeInfo;
48import android.provider.DocumentsContract;
49import android.text.TextUtils;
50import android.util.Log;
51
Ben Kwae3aee182016-02-02 12:11:10 -080052import java.io.File;
53import java.io.IOException;
54import java.util.List;
55
Felipe Lemeb012f912016-01-22 16:49:55 -080056/**
57 * Activity responsible for handling {@link Intent#ACTION_OPEN_EXTERNAL_DOCUMENT}.
58 */
59public class OpenExternalDirectoryActivity extends Activity {
Ben Kwae3aee182016-02-02 12:11:10 -080060 private static final String TAG = "OpenExternalDirectory";
Felipe Lemeb012f912016-01-22 16:49:55 -080061 private static final String FM_TAG = "open_external_directory";
62 private static final String EXTERNAL_STORAGE_AUTH = "com.android.externalstorage.documents";
63 private static final String EXTRA_FILE = "com.android.documentsui.FILE";
64 private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
65 private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
66
Felipe Leme560d23a2016-02-17 17:10:45 -080067 private ContentProviderClient mExternalStorageClient;
68
Felipe Lemeb012f912016-01-22 16:49:55 -080069 @Override
70 public void onCreate(Bundle savedInstanceState) {
71 super.onCreate(savedInstanceState);
72
73 final Intent intent = getIntent();
Felipe Leme34a9d522016-02-17 10:12:04 -080074 if (intent == null) {
75 if (DEBUG) Log.d(TAG, "missing intent");
76 setResult(RESULT_CANCELED);
77 finish();
78 return;
79 }
80 final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
81 if (!(storageVolume instanceof StorageVolume)) {
82 if (DEBUG)
83 Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
84 + storageVolume);
85 setResult(RESULT_CANCELED);
86 finish();
87 return;
88 }
89 final String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME);
90 if (directoryName == null) {
91 if (DEBUG) Log.d(TAG, "missing extra " + EXTRA_DIRECTORY_NAME + " on " + intent);
Felipe Lemeb012f912016-01-22 16:49:55 -080092 setResult(RESULT_CANCELED);
93 finish();
94 return;
95 }
96
Felipe Lemeb012f912016-01-22 16:49:55 -080097 final int userId = UserHandle.myUserId();
Felipe Leme34a9d522016-02-17 10:12:04 -080098 if (!showFragment(this, userId, (StorageVolume) storageVolume, directoryName)) {
Felipe Lemeb012f912016-01-22 16:49:55 -080099 setResult(RESULT_CANCELED);
100 finish();
101 return;
102 }
103 }
104
Felipe Leme560d23a2016-02-17 17:10:45 -0800105 @Override
106 public void onDestroy() {
107 super.onDestroy();
108 if (mExternalStorageClient != null) {
109 mExternalStorageClient.close();
110 }
111 }
112
Felipe Lemeb012f912016-01-22 16:49:55 -0800113 /**
Felipe Leme34a9d522016-02-17 10:12:04 -0800114 * Validates the given path (volume + directory) and display the appropriate dialog asking the
115 * user to grant access to it.
Felipe Lemeb012f912016-01-22 16:49:55 -0800116 */
Felipe Leme560d23a2016-02-17 17:10:45 -0800117 private static boolean showFragment(OpenExternalDirectoryActivity activity, int userId,
118 StorageVolume storageVolume, String directoryName) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800119 if (DEBUG)
120 Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
121 + directoryName + ", and user " + userId);
Felipe Lemeb012f912016-01-22 16:49:55 -0800122 File file;
123 try {
Felipe Leme34a9d522016-02-17 10:12:04 -0800124 file = new File(storageVolume.getPathFile(), directoryName).getCanonicalFile();
Felipe Lemeb012f912016-01-22 16:49:55 -0800125 } catch (IOException e) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800126 Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
127 + " and directory " + directoryName);
Felipe Lemeb012f912016-01-22 16:49:55 -0800128 return false;
129 }
130 final StorageManager sm =
131 (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
132
133 final String root = file.getParent();
134 final String directory = file.getName();
135
136 // Verify directory is valid.
137 if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800138 if (DEBUG)
139 Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
140 + file.getAbsolutePath() + "')");
Felipe Lemeb012f912016-01-22 16:49:55 -0800141 return false;
142 }
143
Felipe Leme560d23a2016-02-17 17:10:45 -0800144 // Gets volume label and converted path.
Felipe Lemeb012f912016-01-22 16:49:55 -0800145 String volumeLabel = null;
146 final List<VolumeInfo> volumes = sm.getVolumes();
147 if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size());
148 for (VolumeInfo volume : volumes) {
149 if (isRightVolume(volume, root, userId)) {
150 final File internalRoot = volume.getInternalPathForUser(userId);
151 // Must convert path before calling getDocIdForFileCreateNewDir()
152 if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot);
153 file = new File(internalRoot, directory);
154 volumeLabel = sm.getBestVolumeDescription(volume);
155 break;
156 }
157 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800158
159 // Checks if the user has granted the permission already.
160 final Intent intent = getIntentForExistingPermission(activity, file);
161 if (intent != null) {
162 activity.setResult(RESULT_OK, intent);
163 activity.finish();
164 return true;
165 }
166
Felipe Lemeb012f912016-01-22 16:49:55 -0800167 if (volumeLabel == null) {
Felipe Leme34a9d522016-02-17 10:12:04 -0800168 Log.e(TAG, "Could not get volume for " + file);
Felipe Lemeb012f912016-01-22 16:49:55 -0800169 return false;
170 }
171
172 // Gets the package label.
173 final String appLabel = getAppLabel(activity);
174 if (appLabel == null) {
175 return false;
176 }
177
178 // Sets args that will be retrieve on onCreate()
179 final Bundle args = new Bundle();
180 args.putString(EXTRA_FILE, file.getAbsolutePath());
181 args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
182 args.putString(EXTRA_APP_LABEL, appLabel);
183
184 final FragmentManager fm = activity.getFragmentManager();
185 final FragmentTransaction ft = fm.beginTransaction();
186 final OpenExternalDirectoryDialogFragment fragment =
187 new OpenExternalDirectoryDialogFragment();
188 fragment.setArguments(args);
189 ft.add(fragment, FM_TAG);
190 ft.commitAllowingStateLoss();
191
192 return true;
193 }
194
195 private static String getAppLabel(Activity activity) {
196 final String packageName = activity.getCallingPackage();
197 final PackageManager pm = activity.getPackageManager();
198 try {
199 return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
200 } catch (NameNotFoundException e) {
201 Log.w(TAG, "Could not get label for package " + packageName);
202 return null;
203 }
204 }
205
206 private static boolean isRightVolume(VolumeInfo volume, String root, int userId) {
207 final File userPath = volume.getPathForUser(userId);
208 final String path = userPath == null ? null : volume.getPathForUser(userId).getPath();
209 final boolean isVisible = volume.isVisibleForWrite(userId);
Felipe Leme34a9d522016-02-17 10:12:04 -0800210 if (DEBUG)
Felipe Lemeb012f912016-01-22 16:49:55 -0800211 Log.d(TAG, "Volume: " + volume + " userId: " + userId + " root: " + root
212 + " volumePath: " + volume.getPath().getPath()
213 + " pathForUser: " + path
214 + " internalPathForUser: " + volume.getInternalPath()
215 + " isVisible: " + isVisible);
Felipe Leme34a9d522016-02-17 10:12:04 -0800216
Felipe Lemeb012f912016-01-22 16:49:55 -0800217 return volume.isVisibleForWrite(userId) && root.equals(path);
218 }
219
Felipe Leme560d23a2016-02-17 17:10:45 -0800220 private static Uri getGrantedUriPermission(ContentProviderClient provider, File file) {
Felipe Lemeb012f912016-01-22 16:49:55 -0800221 // Calls ExternalStorageProvider to get the doc id for the file
222 final Bundle bundle;
223 try {
224 bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null);
225 } catch (RemoteException e) {
226 Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e);
227 return null;
228 }
229 final String docId = bundle == null ? null : bundle.getString("DOC_ID");
230 if (docId == null) {
231 Log.e(TAG, "Did not get doc id from External Storage provider for " + file);
232 return null;
233 }
234 Log.d(TAG, "doc id for " + file + ": " + docId);
235
236 final Uri uri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_AUTH, docId);
237 if (uri == null) {
238 Log.e(TAG, "Could not get URI for doc id " + docId);
239 return null;
240 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800241 if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri);
Felipe Leme560d23a2016-02-17 17:10:45 -0800242 return uri;
243 }
244
245 private static Intent createGrantedUriPermissionsIntent(ContentProviderClient provider,
246 File file) {
247 final Uri uri = getGrantedUriPermission(provider, file);
248 return createGrantedUriPermissionsIntent(uri);
249 }
250
251 private static Intent createGrantedUriPermissionsIntent(Uri uri) {
Felipe Lemeb012f912016-01-22 16:49:55 -0800252 final Intent intent = new Intent();
253 intent.setData(uri);
254 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
255 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
256 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
257 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
258 return intent;
259 }
260
Felipe Leme560d23a2016-02-17 17:10:45 -0800261 private static Intent getIntentForExistingPermission(OpenExternalDirectoryActivity activity,
262 File file) {
263 final String packageName = activity.getCallingPackage();
264 final Uri grantedUri = getGrantedUriPermission(activity.getExternalStorageClient(), file);
265 if (DEBUG)
266 Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri);
267 final ActivityManager am =
268 (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
269 for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
270 final Uri uri = uriPermission.getUri();
271 if (uri.equals(grantedUri)) {
272 if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
273 return createGrantedUriPermissionsIntent(uri);
274 }
275 }
276 if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
277 return null;
278 }
279
Ben Kwae3aee182016-02-02 12:11:10 -0800280 public static class OpenExternalDirectoryDialogFragment extends DialogFragment {
Felipe Lemeb012f912016-01-22 16:49:55 -0800281
282 private File mFile;
283 private String mVolumeLabel;
284 private String mAppLabel;
Felipe Leme560d23a2016-02-17 17:10:45 -0800285 private OpenExternalDirectoryActivity mActivity;
Felipe Lemeb012f912016-01-22 16:49:55 -0800286
287 @Override
288 public void onCreate(Bundle savedInstanceState) {
289 super.onCreate(savedInstanceState);
290 final Bundle args = getArguments();
291 if (args != null) {
292 mFile = new File(args.getString(EXTRA_FILE));
293 mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
294 mAppLabel = args.getString(EXTRA_APP_LABEL);
Felipe Lemeb012f912016-01-22 16:49:55 -0800295 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800296 mActivity = (OpenExternalDirectoryActivity) getActivity();
Felipe Lemeb012f912016-01-22 16:49:55 -0800297 }
298
299 @Override
300 public Dialog onCreateDialog(Bundle savedInstanceState) {
301 final String folder = mFile.getName();
302 final Activity activity = getActivity();
303 final OnClickListener listener = new OnClickListener() {
304
305 @Override
306 public void onClick(DialogInterface dialog, int which) {
307 Intent intent = null;
308 if (which == DialogInterface.BUTTON_POSITIVE) {
Felipe Leme560d23a2016-02-17 17:10:45 -0800309 intent = createGrantedUriPermissionsIntent(
310 mActivity.getExternalStorageClient(), mFile);
Felipe Lemeb012f912016-01-22 16:49:55 -0800311 }
312 if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
313 activity.setResult(RESULT_CANCELED);
314 } else {
315 activity.setResult(RESULT_OK, intent);
316 }
317 activity.finish();
318 }
319 };
320
321 final CharSequence message = TextUtils
322 .expandTemplate(
323 getText(R.string.open_external_dialog_request), mAppLabel, folder,
324 mVolumeLabel);
325 return new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
326 .setMessage(message)
327 .setPositiveButton(R.string.allow, listener)
328 .setNegativeButton(R.string.deny, listener)
329 .create();
330 }
331
332 @Override
333 public void onCancel(DialogInterface dialog) {
334 super.onCancel(dialog);
335 final Activity activity = getActivity();
336 activity.setResult(RESULT_CANCELED);
337 activity.finish();
338 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800339 }
Felipe Lemeb012f912016-01-22 16:49:55 -0800340
Felipe Leme560d23a2016-02-17 17:10:45 -0800341 private synchronized ContentProviderClient getExternalStorageClient() {
342 if (mExternalStorageClient == null) {
343 mExternalStorageClient =
344 getContentResolver().acquireContentProviderClient(EXTERNAL_STORAGE_AUTH);
Felipe Lemeb012f912016-01-22 16:49:55 -0800345 }
Felipe Leme560d23a2016-02-17 17:10:45 -0800346 return mExternalStorageClient;
Felipe Lemeb012f912016-01-22 16:49:55 -0800347 }
348}