diff options
6 files changed, 378 insertions, 12 deletions
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index ba215bb9c754..c776ef82d526 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -402,7 +402,7 @@ public class Environment { * type. */ public static String DIRECTORY_PODCASTS = "Podcasts"; - + /** * Standard directory in which to place any audio files that should be * in the list of ringtones that the user can select (not as regular @@ -414,7 +414,7 @@ public class Environment { * type. */ public static String DIRECTORY_RINGTONES = "Ringtones"; - + /** * Standard directory in which to place any audio files that should be * in the list of alarms that the user can select (not as regular @@ -426,7 +426,7 @@ public class Environment { * type. */ public static String DIRECTORY_ALARMS = "Alarms"; - + /** * Standard directory in which to place any audio files that should be * in the list of notifications that the user can select (not as regular @@ -438,7 +438,7 @@ public class Environment { * type. */ public static String DIRECTORY_NOTIFICATIONS = "Notifications"; - + /** * Standard directory in which to place pictures that are available to * the user. Note that this is primarily a convention for the top-level @@ -446,7 +446,7 @@ public class Environment { * in any directory. */ public static String DIRECTORY_PICTURES = "Pictures"; - + /** * Standard directory in which to place movies that are available to * the user. Note that this is primarily a convention for the top-level @@ -454,7 +454,7 @@ public class Environment { * in any directory. */ public static String DIRECTORY_MOVIES = "Movies"; - + /** * Standard directory in which to place files that have been downloaded by * the user. Note that this is primarily a convention for the top-level @@ -464,7 +464,7 @@ public class Environment { * backwards compatibility reasons. */ public static String DIRECTORY_DOWNLOADS = "Download"; - + /** * The traditional location for pictures and videos when mounting the * device as a camera. Note that this is primarily a convention for the @@ -496,7 +496,7 @@ public class Environment { * </ul> * @hide */ - public static final String[] STANDARD_DIRECTORIES = { + private static final String[] STANDARD_DIRECTORIES = { DIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, @@ -510,6 +510,18 @@ public class Environment { }; /** + * @hide + */ + public static boolean isStandardDirectory(String dir) { + for (String valid : STANDARD_DIRECTORIES) { + if (valid.equals(dir)) { + return true; + } + } + return false; + } + + /** * Get a top-level shared/external storage directory for placing files of a * particular type. This is where the user will typically place and manage * their own files, so you should be careful about what you put here to @@ -559,7 +571,7 @@ public class Environment { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppDataDirs(packageName); } - + /** * Generates the raw path to an application's media * @hide @@ -568,7 +580,7 @@ public class Environment { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppMediaDirs(packageName); } - + /** * Generates the raw path to an application's OBB files * @hide @@ -577,7 +589,7 @@ public class Environment { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppObbDirs(packageName); } - + /** * Generates the path to an application's files. * @hide @@ -595,7 +607,7 @@ public class Environment { throwIfUserRequired(); return sCurrentUser.buildExternalStorageAppCacheDirs(packageName); } - + /** * Return the download/cache content directory. */ diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml index 6beef4415f99..c3452d585a6e 100644 --- a/packages/DocumentsUI/AndroidManifest.xml +++ b/packages/DocumentsUI/AndroidManifest.xml @@ -78,6 +78,16 @@ </intent-filter> </activity> + <activity + android:name=".OpenExternalDirectoryActivity" + android:theme="@android:style/Theme.Translucent.NoTitleBar"> + <intent-filter> + <action android:name="android.intent.action.OPEN_EXTERNAL_DIRECTORY" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="file" /> + </intent-filter> + </activity> + <provider android:name=".RecentsProvider" android:authorities="com.android.documentsui.recents" diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml index 5a9bc162f3d6..ff7b7b9fed07 100644 --- a/packages/DocumentsUI/res/values/strings.xml +++ b/packages/DocumentsUI/res/values/strings.xml @@ -196,4 +196,13 @@ <string name="menu_rename">Rename</string> <!-- Toast shown when renaming document failed with an error [CHAR LIMIT=48] --> <string name="rename_error">Failed to rename document</string> + + <!-- DO NOT TRANSLATE - final phrase has not been decided yet (b/26750152) --> + <string name="open_external_dialog_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g> + access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> folder on + <xliff:g id="storage" example="SD Card"><i>^3</i></xliff:g>?</string> + <!-- Text in the button asking user to allow access to a given directory. --> + <string name="allow">Allow</string> + <!-- Text in the button asking user to deny access to a given directory. --> + <string name="deny">Deny</string> </resources> diff --git a/packages/DocumentsUI/res/values/styles.xml b/packages/DocumentsUI/res/values/styles.xml index d14631dbbc1a..f4dfd73690ca 100644 --- a/packages/DocumentsUI/res/values/styles.xml +++ b/packages/DocumentsUI/res/values/styles.xml @@ -45,4 +45,8 @@ <item name="android:maxHeight">3dp</item> </style> + <!-- TODO: use the proper dialog and/or inline if not overriding --> + <style name="AlertDialogTheme" parent="@style/Theme.AppCompat.Light.Dialog.Alert"> + </style> + </resources> diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java new file mode 100644 index 000000000000..5dc4f57bdb7b --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import static android.os.Environment.isStandardDirectory; +import static com.android.documentsui.Shared.DEBUG; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.ContentProvider; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.storage.StorageManager; +import android.os.storage.VolumeInfo; +import android.provider.DocumentsContract; +import android.text.TextUtils; +import android.util.Log; + +/** + * Activity responsible for handling {@link Intent#ACTION_OPEN_EXTERNAL_DOCUMENT}. + */ +public class OpenExternalDirectoryActivity extends Activity { + private static final String TAG = "OpenExternalDirectoryActivity"; + private static final String FM_TAG = "open_external_directory"; + private static final String EXTERNAL_STORAGE_AUTH = "com.android.externalstorage.documents"; + private static final String EXTRA_FILE = "com.android.documentsui.FILE"; + private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL"; + private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + if (intent == null || intent.getData() == null) { + Log.d(TAG, "missing intent or intent data: " + intent); + setResult(RESULT_CANCELED); + finish(); + return; + } + + final String path = intent.getData().getPath(); + final int userId = UserHandle.myUserId(); + if (!showFragment(this, userId, path)) { + setResult(RESULT_CANCELED); + finish(); + return; + } + } + + /** + * Validates the given {@code path} and display the appropriate dialog asking the user to grant + * access to it. + */ + static boolean showFragment(Activity activity, int userId, String path) { + Log.d(TAG, "showFragment() for path " + path + " and user " + userId); + if (path == null) { + Log.e(TAG, "INTERNAL ERROR: showFragment() with null path"); + return false; + } + File file; + try { + file = new File(new File(path).getCanonicalPath()); + } catch (IOException e) { + Log.e(TAG, "Could not get canonical file from " + path); + return false; + } + final StorageManager sm = + (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE); + + final String root = file.getParent(); + final String directory = file.getName(); + + // Verify directory is valid. + if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) { + Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '" + path + "')"); + return false; + } + + // Gets volume label and converted path + String volumeLabel = null; + final List<VolumeInfo> volumes = sm.getVolumes(); + if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); + for (VolumeInfo volume : volumes) { + if (isRightVolume(volume, root, userId)) { + final File internalRoot = volume.getInternalPathForUser(userId); + // Must convert path before calling getDocIdForFileCreateNewDir() + if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); + file = new File(internalRoot, directory); + volumeLabel = sm.getBestVolumeDescription(volume); + break; + } + } + if (volumeLabel == null) { + Log.e(TAG, "Could not get volume for " + path); + return false; + } + + // Gets the package label. + final String appLabel = getAppLabel(activity); + if (appLabel == null) { + return false; + } + + // Sets args that will be retrieve on onCreate() + final Bundle args = new Bundle(); + args.putString(EXTRA_FILE, file.getAbsolutePath()); + args.putString(EXTRA_VOLUME_LABEL, volumeLabel); + args.putString(EXTRA_APP_LABEL, appLabel); + + final FragmentManager fm = activity.getFragmentManager(); + final FragmentTransaction ft = fm.beginTransaction(); + final OpenExternalDirectoryDialogFragment fragment = + new OpenExternalDirectoryDialogFragment(); + fragment.setArguments(args); + ft.add(fragment, FM_TAG); + ft.commitAllowingStateLoss(); + + return true; + } + + private static String getAppLabel(Activity activity) { + final String packageName = activity.getCallingPackage(); + final PackageManager pm = activity.getPackageManager(); + try { + return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString(); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could not get label for package " + packageName); + return null; + } + } + + private static boolean isRightVolume(VolumeInfo volume, String root, int userId) { + final File userPath = volume.getPathForUser(userId); + final String path = userPath == null ? null : volume.getPathForUser(userId).getPath(); + final boolean isVisible = volume.isVisibleForWrite(userId); + if (DEBUG) { + Log.d(TAG, "Volume: " + volume + " userId: " + userId + " root: " + root + + " volumePath: " + volume.getPath().getPath() + + " pathForUser: " + path + + " internalPathForUser: " + volume.getInternalPath() + + " isVisible: " + isVisible); + } + return volume.isVisibleForWrite(userId) && root.equals(path); + } + + private static Intent createGrantedUriPermissionsIntent(ContentProviderClient provider, + File file) { + // Calls ExternalStorageProvider to get the doc id for the file + final Bundle bundle; + try { + bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null); + } catch (RemoteException e) { + Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e); + return null; + } + final String docId = bundle == null ? null : bundle.getString("DOC_ID"); + if (docId == null) { + Log.e(TAG, "Did not get doc id from External Storage provider for " + file); + return null; + } + Log.d(TAG, "doc id for " + file + ": " + docId); + + final Uri uri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_AUTH, docId); + if (uri == null) { + Log.e(TAG, "Could not get URI for doc id " + docId); + return null; + } + + if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri); + final Intent intent = new Intent(); + intent.setData(uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + return intent; + } + + private static class OpenExternalDirectoryDialogFragment extends DialogFragment { + + private File mFile; + private String mVolumeLabel; + private String mAppLabel; + private ContentProviderClient mExternalStorageClient; + private ContentResolver mResolver; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + if (args != null) { + mFile = new File(args.getString(EXTRA_FILE)); + mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL); + mAppLabel = args.getString(EXTRA_APP_LABEL); + mResolver = getContext().getContentResolver(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mExternalStorageClient != null) { + mExternalStorageClient.close(); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final String folder = mFile.getName(); + final Activity activity = getActivity(); + final OnClickListener listener = new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = null; + if (which == DialogInterface.BUTTON_POSITIVE) { + intent = createGrantedUriPermissionsIntent(getExternalStorageClient(), + mFile); + } + if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) { + activity.setResult(RESULT_CANCELED); + } else { + activity.setResult(RESULT_OK, intent); + } + activity.finish(); + } + }; + + final CharSequence message = TextUtils + .expandTemplate( + getText(R.string.open_external_dialog_request), mAppLabel, folder, + mVolumeLabel); + return new AlertDialog.Builder(activity, R.style.AlertDialogTheme) + .setMessage(message) + .setPositiveButton(R.string.allow, listener) + .setNegativeButton(R.string.deny, listener) + .create(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + final Activity activity = getActivity(); + activity.setResult(RESULT_CANCELED); + activity.finish(); + } + + private synchronized ContentProviderClient getExternalStorageClient() { + if (mExternalStorageClient == null) { + mExternalStorageClient = + mResolver.acquireContentProviderClient(EXTERNAL_STORAGE_AUTH); + } + return mExternalStorageClient; + } + } +} diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index 43527a23539c..1bfc19c101eb 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -25,6 +25,7 @@ import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.graphics.Point; import android.net.Uri; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.FileObserver; import android.os.FileUtils; @@ -234,7 +235,13 @@ public class ExternalStorageProvider extends DocumentsProvider { return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; } + private String getDocIdForFile(File file) throws FileNotFoundException { + return getDocIdForFileMaybeCreate(file, false); + } + + private String getDocIdForFileMaybeCreate(File file, boolean createNewDir) + throws FileNotFoundException { String path = file.getAbsolutePath(); // Find the most-specific root path @@ -266,6 +273,13 @@ public class ExternalStorageProvider extends DocumentsProvider { path = path.substring(rootPath.length() + 1); } + if (!file.exists() && createNewDir) { + Log.i(TAG, "Creating new directory " + file); + if (!file.mkdir()) { + Log.e(TAG, "Could not create directory " + file); + } + } + return mostSpecificId + ':' + path; } @@ -609,6 +623,34 @@ public class ExternalStorageProvider extends DocumentsProvider { } } + @Override + public Bundle call(String method, String arg, Bundle extras) { + Bundle bundle = super.call(method, arg, extras); + if (bundle == null && !TextUtils.isEmpty(method)) { + switch (method) { + case "getDocIdForFileCreateNewDir": { + getContext().enforceCallingPermission( + android.Manifest.permission.MANAGE_DOCUMENTS, null); + if (TextUtils.isEmpty(arg)) { + return null; + } + try { + final String docId = getDocIdForFileMaybeCreate(new File(arg), true); + bundle = new Bundle(); + bundle.putString("DOC_ID", docId); + } catch (FileNotFoundException e) { + Log.w(TAG, "file '" + arg + "' not found"); + return null; + } + break; + } + default: + Log.w(TAG, "unknown method passed to call(): " + method); + } + } + return bundle; + } + private static String getTypeForFile(File file) { if (file.isDirectory()) { return Document.MIME_TYPE_DIR; |