Loading packages/DocumentsUI/res/layout/dialog_open_scoped_directory.xml 0 → 100644 +45 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:paddingEnd="24dp" android:paddingStart="24dp" > <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/message" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingEnd="24dp" android:paddingStart="24dp" android:paddingTop="24dp" android:textAppearance="@android:style/TextAppearance.Material.Subhead" android:textColor="@*android:color/primary_text_default_material_light" > </TextView> <CheckBox android:id="@+id/do_not_ask_checkbox" style="?android:attr/textAppearanceSmall" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="16dip" android:text="@string/never_ask_again" android:visibility="gone" /> </LinearLayout> No newline at end of file packages/DocumentsUI/res/values/strings.xml +4 −2 Original line number Diff line number Diff line Loading @@ -200,10 +200,12 @@ during a copy. [CHAR LIMIT=48] --> <string name="notification_copy_files_converted_title">Some files were converted</string> <!-- DO NOT TRANSLATE - final phrase has not been decided yet (b/26750152) --> <!-- Text in an alert dialog asking user to grant app access to a given directory in an external storage volume --> <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 access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> directory on <xliff:g id="storage" example="SD Card"><i>^3</i></xliff:g>?</string> <!-- Checkbox that allows user to not be questioned about the directory access request again --> <string name="never_ask_again">Don\'t ask again</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. --> Loading packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java +64 −10 Original line number Diff line number Diff line Loading @@ -16,10 +16,20 @@ package com.android.documentsui; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Shared.TAG; import static com.android.documentsui.State.MODE_UNKNOWN; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; import android.content.SharedPreferences; import android.os.UserHandle; import android.preference.PreferenceManager; import android.util.Log; import com.android.documentsui.State.ViewMode; import com.android.documentsui.model.RootInfo; Loading @@ -29,29 +39,73 @@ public class LocalPreferences { private static final String ROOT_VIEW_MODE_PREFIX = "rootViewMode-"; public static boolean getDisplayFileSize(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(KEY_FILE_SIZE, false); return getPrefs(context).getBoolean(KEY_FILE_SIZE, false); } public static @ViewMode int getViewMode( Context context, RootInfo root, @ViewMode int fallback) { return PreferenceManager.getDefaultSharedPreferences(context) .getInt(createKey(root), fallback); public static @ViewMode int getViewMode(Context context, RootInfo root, @ViewMode int fallback) { return getPrefs(context).getInt(createKey(root), fallback); } public static void setDisplayFileSize(Context context, boolean display) { PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean(KEY_FILE_SIZE, display).apply(); getPrefs(context).edit().putBoolean(KEY_FILE_SIZE, display).apply(); } public static void setViewMode(Context context, RootInfo root, @ViewMode int viewMode) { assert(viewMode != MODE_UNKNOWN); PreferenceManager.getDefaultSharedPreferences(context).edit() .putInt(createKey(root), viewMode).apply(); getPrefs(context).edit().putInt(createKey(root), viewMode).apply(); } private static SharedPreferences getPrefs(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } private static String createKey(RootInfo root) { return ROOT_VIEW_MODE_PREFIX + root.authority + root.rootId; } public static final int PERMISSION_ASK = 0; public static final int PERMISSION_ASK_AGAIN = 1; public static final int PERMISSION_NEVER_ASK = -1; @IntDef(flag = true, value = { PERMISSION_ASK, PERMISSION_ASK_AGAIN, PERMISSION_NEVER_ASK, }) @Retention(RetentionPolicy.SOURCE) public @interface PermissionStatus {} /** * Methods below are used to keep track of denied user requests on scoped directory access so * the dialog is not offered when user checked the 'Do not ask again' box * * <p>It uses a shared preferences, whose key is: * <ol> * <li>{@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID * (typically physical volumes like SD cards). * <li>{@code USER_ID|PACKAGE_NAME||DIRECTORY} for storage volumes that do not have a UUID * (typically the emulated volume used for primary storage * </ol> */ static @PermissionStatus int getScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory) { final String key = getScopedAccessDenialsKey(packageName, uuid, directory); return getPrefs(context).getInt(key, PERMISSION_ASK); } static void setScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory, @PermissionStatus int status) { final String key = getScopedAccessDenialsKey(packageName, uuid, directory); getPrefs(context).edit().putInt(key, status).apply(); } private static String getScopedAccessDenialsKey(String packageName, String uuid, String directory) { final int userId = UserHandle.myUserId(); return uuid == null ? userId + "|" + packageName + "||" + directory : userId + "|" + packageName + "|" + uuid + "|" + directory; } } packages/DocumentsUI/src/com/android/documentsui/Metrics.java +29 −14 Original line number Diff line number Diff line Loading @@ -411,11 +411,15 @@ public final class Metrics { public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED = 0; public static final int SCOPED_DIRECTORY_ACCESS_GRANTED = 1; public static final int SCOPED_DIRECTORY_ACCESS_DENIED = 2; public static final int SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST = 3; public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED = 4; @IntDef(flag = true, value = { SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED, SCOPED_DIRECTORY_ACCESS_GRANTED, SCOPED_DIRECTORY_ACCESS_DENIED SCOPED_DIRECTORY_ACCESS_DENIED, SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST, SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED }) @Retention(RetentionPolicy.SOURCE) public @interface ScopedAccessGrant {} Loading @@ -432,23 +436,34 @@ public final class Metrics { final String packageName = activity.getCallingPackage(); switch (type) { case SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED: MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_GRANTED: MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED: MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER, index); break; default: Log.wtf(TAG, "invalid ScopedAccessGrant: " + type); Loading packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java +96 −13 Original line number Diff line number Diff line Loading @@ -20,16 +20,24 @@ import static android.os.Environment.isStandardDirectory; import static android.os.Environment.STANDARD_DIRECTORIES; import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME; import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest; import static com.android.documentsui.Metrics.logValidScopedAccessRequest; import static com.android.documentsui.LocalPreferences.getScopedAccessPermissionStatus; import static com.android.documentsui.LocalPreferences.PERMISSION_ASK; import static com.android.documentsui.LocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.LocalPreferences.PERMISSION_NEVER_ASK; import static com.android.documentsui.LocalPreferences.setScopedAccessPermissionStatus; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ERROR; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_GRANTED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest; import static com.android.documentsui.Metrics.logValidScopedAccessRequest; import static com.android.documentsui.Shared.DEBUG; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; Loading @@ -38,7 +46,6 @@ import android.app.DialogFragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; Loading @@ -57,6 +64,11 @@ import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.TextView; import java.io.File; import java.io.IOException; Loading @@ -72,12 +84,17 @@ public class OpenExternalDirectoryActivity extends Activity { 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"; private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID"; private ContentProviderClient mExternalStorageClient; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { if (DEBUG) Log.d(TAG, "activity.onCreateDialog(): reusing instance"); return; } final Intent intent = getIntent(); if (intent == null) { Loading Loading @@ -105,9 +122,18 @@ public class OpenExternalDirectoryActivity extends Activity { finish(); return; } final StorageVolume volume = (StorageVolume) storageVolume; if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(), volume.getUuid(), directoryName) == PERMISSION_NEVER_ASK) { logValidScopedAccessRequest(this, directoryName, SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED); setResult(RESULT_CANCELED); finish(); return; } final int userId = UserHandle.myUserId(); if (!showFragment(this, userId, (StorageVolume) storageVolume, directoryName)) { if (!showFragment(this, userId, volume, directoryName)) { setResult(RESULT_CANCELED); finish(); return; Loading Loading @@ -157,6 +183,7 @@ public class OpenExternalDirectoryActivity extends Activity { // Gets volume label and converted path. String volumeLabel = null; String volumeUuid = null; final List<VolumeInfo> volumes = sm.getVolumes(); if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); for (VolumeInfo volume : volumes) { Loading @@ -166,6 +193,7 @@ public class OpenExternalDirectoryActivity extends Activity { if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = new File(internalRoot, directory); volumeLabel = sm.getBestVolumeDescription(volume); volumeUuid = volume.getFsUuid(); break; } } Loading Loading @@ -197,6 +225,7 @@ public class OpenExternalDirectoryActivity extends Activity { final Bundle args = new Bundle(); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_UUID, volumeUuid); args.putString(EXTRA_APP_LABEL, appLabel); final FragmentManager fm = activity.getFragmentManager(); Loading Loading @@ -303,26 +332,50 @@ public class OpenExternalDirectoryActivity extends Activity { public static class OpenExternalDirectoryDialogFragment extends DialogFragment { private File mFile; private String mVolumeUuid; private String mVolumeLabel; private String mAppLabel; private CheckBox mDontAskAgain; private OpenExternalDirectoryActivity mActivity; private AlertDialog mDialog; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); final Bundle args = getArguments(); if (args != null) { mFile = new File(args.getString(EXTRA_FILE)); mVolumeUuid = args.getString(EXTRA_VOLUME_UUID); mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL); mAppLabel = args.getString(EXTRA_APP_LABEL); } mActivity = (OpenExternalDirectoryActivity) getActivity(); } @Override public void onDestroyView() { // Workaround for https://code.google.com/p/android/issues/detail?id=17423 if (mDialog != null && getRetainInstance()) { mDialog.setDismissMessage(null); } super.onDestroyView(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { if (mDialog != null) { if (DEBUG) Log.d(TAG, "fragment.onCreateDialog(): reusing dialog"); return mDialog; } if (mActivity != getActivity()) { // Sanity check. Log.wtf(TAG, "activity references don't match on onCreateDialog(): mActivity = " + mActivity + " , getActivity() = " + getActivity()); mActivity = (OpenExternalDirectoryActivity) getActivity(); } final String directory = mFile.getName(); final Activity activity = getActivity(); final Context context = mActivity.getApplicationContext(); final OnClickListener listener = new OnClickListener() { @Override Loading @@ -333,15 +386,25 @@ public class OpenExternalDirectoryActivity extends Activity { mActivity.getExternalStorageClient(), mFile); } if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) { logValidScopedAccessRequest(activity, directory, logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_DENIED); activity.setResult(RESULT_CANCELED); final boolean checked = mDontAskAgain.isChecked(); if (checked) { logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST); setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directory, PERMISSION_NEVER_ASK); } else { logValidScopedAccessRequest(activity, directory, setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directory, PERMISSION_ASK_AGAIN); } mActivity.setResult(RESULT_CANCELED); } else { logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_GRANTED); activity.setResult(RESULT_OK, intent); mActivity.setResult(RESULT_OK, intent); } activity.finish(); mActivity.finish(); } }; Loading @@ -349,11 +412,31 @@ public class OpenExternalDirectoryActivity extends Activity { .expandTemplate( getText(R.string.open_external_dialog_request), mAppLabel, directory, mVolumeLabel); return new AlertDialog.Builder(activity, R.style.AlertDialogTheme) .setMessage(message) @SuppressLint("InflateParams") // It's ok pass null ViewRoot on AlertDialogs. final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null); final TextView messageField = (TextView) view.findViewById(R.id.message); messageField.setText(message); mDialog = new AlertDialog.Builder(mActivity, R.style.AlertDialogTheme) .setView(view) .setPositiveButton(R.string.allow, listener) .setNegativeButton(R.string.deny, listener) .create(); mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox); if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directory) == PERMISSION_ASK_AGAIN) { mDontAskAgain.setVisibility(View.VISIBLE); mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked); } }); } return mDialog; } @Override Loading Loading
packages/DocumentsUI/res/layout/dialog_open_scoped_directory.xml 0 → 100644 +45 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:paddingEnd="24dp" android:paddingStart="24dp" > <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/message" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingEnd="24dp" android:paddingStart="24dp" android:paddingTop="24dp" android:textAppearance="@android:style/TextAppearance.Material.Subhead" android:textColor="@*android:color/primary_text_default_material_light" > </TextView> <CheckBox android:id="@+id/do_not_ask_checkbox" style="?android:attr/textAppearanceSmall" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="16dip" android:text="@string/never_ask_again" android:visibility="gone" /> </LinearLayout> No newline at end of file
packages/DocumentsUI/res/values/strings.xml +4 −2 Original line number Diff line number Diff line Loading @@ -200,10 +200,12 @@ during a copy. [CHAR LIMIT=48] --> <string name="notification_copy_files_converted_title">Some files were converted</string> <!-- DO NOT TRANSLATE - final phrase has not been decided yet (b/26750152) --> <!-- Text in an alert dialog asking user to grant app access to a given directory in an external storage volume --> <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 access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> directory on <xliff:g id="storage" example="SD Card"><i>^3</i></xliff:g>?</string> <!-- Checkbox that allows user to not be questioned about the directory access request again --> <string name="never_ask_again">Don\'t ask again</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. --> Loading
packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java +64 −10 Original line number Diff line number Diff line Loading @@ -16,10 +16,20 @@ package com.android.documentsui; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Shared.TAG; import static com.android.documentsui.State.MODE_UNKNOWN; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; import android.content.SharedPreferences; import android.os.UserHandle; import android.preference.PreferenceManager; import android.util.Log; import com.android.documentsui.State.ViewMode; import com.android.documentsui.model.RootInfo; Loading @@ -29,29 +39,73 @@ public class LocalPreferences { private static final String ROOT_VIEW_MODE_PREFIX = "rootViewMode-"; public static boolean getDisplayFileSize(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(KEY_FILE_SIZE, false); return getPrefs(context).getBoolean(KEY_FILE_SIZE, false); } public static @ViewMode int getViewMode( Context context, RootInfo root, @ViewMode int fallback) { return PreferenceManager.getDefaultSharedPreferences(context) .getInt(createKey(root), fallback); public static @ViewMode int getViewMode(Context context, RootInfo root, @ViewMode int fallback) { return getPrefs(context).getInt(createKey(root), fallback); } public static void setDisplayFileSize(Context context, boolean display) { PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean(KEY_FILE_SIZE, display).apply(); getPrefs(context).edit().putBoolean(KEY_FILE_SIZE, display).apply(); } public static void setViewMode(Context context, RootInfo root, @ViewMode int viewMode) { assert(viewMode != MODE_UNKNOWN); PreferenceManager.getDefaultSharedPreferences(context).edit() .putInt(createKey(root), viewMode).apply(); getPrefs(context).edit().putInt(createKey(root), viewMode).apply(); } private static SharedPreferences getPrefs(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } private static String createKey(RootInfo root) { return ROOT_VIEW_MODE_PREFIX + root.authority + root.rootId; } public static final int PERMISSION_ASK = 0; public static final int PERMISSION_ASK_AGAIN = 1; public static final int PERMISSION_NEVER_ASK = -1; @IntDef(flag = true, value = { PERMISSION_ASK, PERMISSION_ASK_AGAIN, PERMISSION_NEVER_ASK, }) @Retention(RetentionPolicy.SOURCE) public @interface PermissionStatus {} /** * Methods below are used to keep track of denied user requests on scoped directory access so * the dialog is not offered when user checked the 'Do not ask again' box * * <p>It uses a shared preferences, whose key is: * <ol> * <li>{@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID * (typically physical volumes like SD cards). * <li>{@code USER_ID|PACKAGE_NAME||DIRECTORY} for storage volumes that do not have a UUID * (typically the emulated volume used for primary storage * </ol> */ static @PermissionStatus int getScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory) { final String key = getScopedAccessDenialsKey(packageName, uuid, directory); return getPrefs(context).getInt(key, PERMISSION_ASK); } static void setScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory, @PermissionStatus int status) { final String key = getScopedAccessDenialsKey(packageName, uuid, directory); getPrefs(context).edit().putInt(key, status).apply(); } private static String getScopedAccessDenialsKey(String packageName, String uuid, String directory) { final int userId = UserHandle.myUserId(); return uuid == null ? userId + "|" + packageName + "||" + directory : userId + "|" + packageName + "|" + uuid + "|" + directory; } }
packages/DocumentsUI/src/com/android/documentsui/Metrics.java +29 −14 Original line number Diff line number Diff line Loading @@ -411,11 +411,15 @@ public final class Metrics { public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED = 0; public static final int SCOPED_DIRECTORY_ACCESS_GRANTED = 1; public static final int SCOPED_DIRECTORY_ACCESS_DENIED = 2; public static final int SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST = 3; public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED = 4; @IntDef(flag = true, value = { SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED, SCOPED_DIRECTORY_ACCESS_GRANTED, SCOPED_DIRECTORY_ACCESS_DENIED SCOPED_DIRECTORY_ACCESS_DENIED, SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST, SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED }) @Retention(RetentionPolicy.SOURCE) public @interface ScopedAccessGrant {} Loading @@ -432,23 +436,34 @@ public final class Metrics { final String packageName = activity.getCallingPackage(); switch (type) { case SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED: MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_GRANTED: MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED: MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER, index); break; default: Log.wtf(TAG, "invalid ScopedAccessGrant: " + type); Loading
packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java +96 −13 Original line number Diff line number Diff line Loading @@ -20,16 +20,24 @@ import static android.os.Environment.isStandardDirectory; import static android.os.Environment.STANDARD_DIRECTORIES; import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME; import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest; import static com.android.documentsui.Metrics.logValidScopedAccessRequest; import static com.android.documentsui.LocalPreferences.getScopedAccessPermissionStatus; import static com.android.documentsui.LocalPreferences.PERMISSION_ASK; import static com.android.documentsui.LocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.LocalPreferences.PERMISSION_NEVER_ASK; import static com.android.documentsui.LocalPreferences.setScopedAccessPermissionStatus; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ERROR; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_GRANTED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest; import static com.android.documentsui.Metrics.logValidScopedAccessRequest; import static com.android.documentsui.Shared.DEBUG; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; Loading @@ -38,7 +46,6 @@ import android.app.DialogFragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; Loading @@ -57,6 +64,11 @@ import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.TextView; import java.io.File; import java.io.IOException; Loading @@ -72,12 +84,17 @@ public class OpenExternalDirectoryActivity extends Activity { 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"; private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID"; private ContentProviderClient mExternalStorageClient; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { if (DEBUG) Log.d(TAG, "activity.onCreateDialog(): reusing instance"); return; } final Intent intent = getIntent(); if (intent == null) { Loading Loading @@ -105,9 +122,18 @@ public class OpenExternalDirectoryActivity extends Activity { finish(); return; } final StorageVolume volume = (StorageVolume) storageVolume; if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(), volume.getUuid(), directoryName) == PERMISSION_NEVER_ASK) { logValidScopedAccessRequest(this, directoryName, SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED); setResult(RESULT_CANCELED); finish(); return; } final int userId = UserHandle.myUserId(); if (!showFragment(this, userId, (StorageVolume) storageVolume, directoryName)) { if (!showFragment(this, userId, volume, directoryName)) { setResult(RESULT_CANCELED); finish(); return; Loading Loading @@ -157,6 +183,7 @@ public class OpenExternalDirectoryActivity extends Activity { // Gets volume label and converted path. String volumeLabel = null; String volumeUuid = null; final List<VolumeInfo> volumes = sm.getVolumes(); if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); for (VolumeInfo volume : volumes) { Loading @@ -166,6 +193,7 @@ public class OpenExternalDirectoryActivity extends Activity { if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = new File(internalRoot, directory); volumeLabel = sm.getBestVolumeDescription(volume); volumeUuid = volume.getFsUuid(); break; } } Loading Loading @@ -197,6 +225,7 @@ public class OpenExternalDirectoryActivity extends Activity { final Bundle args = new Bundle(); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_UUID, volumeUuid); args.putString(EXTRA_APP_LABEL, appLabel); final FragmentManager fm = activity.getFragmentManager(); Loading Loading @@ -303,26 +332,50 @@ public class OpenExternalDirectoryActivity extends Activity { public static class OpenExternalDirectoryDialogFragment extends DialogFragment { private File mFile; private String mVolumeUuid; private String mVolumeLabel; private String mAppLabel; private CheckBox mDontAskAgain; private OpenExternalDirectoryActivity mActivity; private AlertDialog mDialog; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); final Bundle args = getArguments(); if (args != null) { mFile = new File(args.getString(EXTRA_FILE)); mVolumeUuid = args.getString(EXTRA_VOLUME_UUID); mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL); mAppLabel = args.getString(EXTRA_APP_LABEL); } mActivity = (OpenExternalDirectoryActivity) getActivity(); } @Override public void onDestroyView() { // Workaround for https://code.google.com/p/android/issues/detail?id=17423 if (mDialog != null && getRetainInstance()) { mDialog.setDismissMessage(null); } super.onDestroyView(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { if (mDialog != null) { if (DEBUG) Log.d(TAG, "fragment.onCreateDialog(): reusing dialog"); return mDialog; } if (mActivity != getActivity()) { // Sanity check. Log.wtf(TAG, "activity references don't match on onCreateDialog(): mActivity = " + mActivity + " , getActivity() = " + getActivity()); mActivity = (OpenExternalDirectoryActivity) getActivity(); } final String directory = mFile.getName(); final Activity activity = getActivity(); final Context context = mActivity.getApplicationContext(); final OnClickListener listener = new OnClickListener() { @Override Loading @@ -333,15 +386,25 @@ public class OpenExternalDirectoryActivity extends Activity { mActivity.getExternalStorageClient(), mFile); } if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) { logValidScopedAccessRequest(activity, directory, logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_DENIED); activity.setResult(RESULT_CANCELED); final boolean checked = mDontAskAgain.isChecked(); if (checked) { logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST); setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directory, PERMISSION_NEVER_ASK); } else { logValidScopedAccessRequest(activity, directory, setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directory, PERMISSION_ASK_AGAIN); } mActivity.setResult(RESULT_CANCELED); } else { logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_GRANTED); activity.setResult(RESULT_OK, intent); mActivity.setResult(RESULT_OK, intent); } activity.finish(); mActivity.finish(); } }; Loading @@ -349,11 +412,31 @@ public class OpenExternalDirectoryActivity extends Activity { .expandTemplate( getText(R.string.open_external_dialog_request), mAppLabel, directory, mVolumeLabel); return new AlertDialog.Builder(activity, R.style.AlertDialogTheme) .setMessage(message) @SuppressLint("InflateParams") // It's ok pass null ViewRoot on AlertDialogs. final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null); final TextView messageField = (TextView) view.findViewById(R.id.message); messageField.setText(message); mDialog = new AlertDialog.Builder(mActivity, R.style.AlertDialogTheme) .setView(view) .setPositiveButton(R.string.allow, listener) .setNegativeButton(R.string.deny, listener) .create(); mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox); if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directory) == PERMISSION_ASK_AGAIN) { mDontAskAgain.setVisibility(View.VISIBLE); mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked); } }); } return mDialog; } @Override Loading