Loading core/java/android/os/Environment.java +24 −12 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -509,6 +509,18 @@ public class Environment { DIRECTORY_DOCUMENTS }; /** * @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 Loading packages/DocumentsUI/AndroidManifest.xml +10 −0 Original line number Diff line number Diff line Loading @@ -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" Loading packages/DocumentsUI/res/values/strings.xml +9 −0 Original line number Diff line number Diff line Loading @@ -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> packages/DocumentsUI/res/values/styles.xml +4 −0 Original line number Diff line number Diff line Loading @@ -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> packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java 0 → 100644 +289 −0 Original line number Diff line number Diff line /* * 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; } } } Loading
core/java/android/os/Environment.java +24 −12 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -509,6 +509,18 @@ public class Environment { DIRECTORY_DOCUMENTS }; /** * @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 Loading
packages/DocumentsUI/AndroidManifest.xml +10 −0 Original line number Diff line number Diff line Loading @@ -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" Loading
packages/DocumentsUI/res/values/strings.xml +9 −0 Original line number Diff line number Diff line Loading @@ -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>
packages/DocumentsUI/res/values/styles.xml +4 −0 Original line number Diff line number Diff line Loading @@ -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>
packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java 0 → 100644 +289 −0 Original line number Diff line number Diff line /* * 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; } } }