From 6d88d79aa5e3e06c7199e47e614ac806e352fabc Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Fri, 19 Jan 2018 17:34:23 -0800 Subject: [PATCH] Finished initial implementation of StorageAccessDetails. It still needs some polishing and unit tests, which will be added later. Test: manual verification Bug: 63720392 Change-Id: I8da1094aaaf4ebbe5d388021e859265091fd429c --- res/xml/directory_access_details.xml | 20 ++ res/xml/special_access.xml | 4 +- .../AppStateDirectoryAccessBridge.java | 27 +- .../applications/DirectoryAccessDetails.java | 236 ++++++++++++++---- 4 files changed, 234 insertions(+), 53 deletions(-) create mode 100644 res/xml/directory_access_details.xml diff --git a/res/xml/directory_access_details.xml b/res/xml/directory_access_details.xml new file mode 100644 index 00000000000..4448ba65aeb --- /dev/null +++ b/res/xml/directory_access_details.xml @@ -0,0 +1,20 @@ + + + + diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml index 0843b79fb16..7205eaf28ab 100644 --- a/res/xml/special_access.xml +++ b/res/xml/special_access.xml @@ -111,7 +111,6 @@ android:value="com.android.settings.Settings$VrListenersSettingsActivity" /> - diff --git a/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java b/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java index 8cd4444ed4c..1c2a0af8a74 100644 --- a/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java +++ b/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java @@ -33,11 +33,15 @@ import com.android.settingslib.applications.ApplicationsState.AppFilter; import java.util.Set; -// TODO(b/63720392): add unit tests +// TODO(b/72055774): add unit tests public class AppStateDirectoryAccessBridge extends AppStateBaseBridge { private static final String TAG = "DirectoryAccessBridge"; + // TODO(b/72055774): set to false once feature is ready (or use Log.isLoggable) + static final boolean DEBUG = true; + static final boolean VERBOSE = true; + public AppStateDirectoryAccessBridge(ApplicationsState appState, Callback callback) { super(appState, callback); } @@ -59,27 +63,34 @@ public class AppStateDirectoryAccessBridge extends AppStateBaseBridge { @Override public void init(Context context) { - try (Cursor cursor = context.getContentResolver().query( - new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY).appendPath(TABLE_PACKAGES).appendPath("*") - .build(), TABLE_PACKAGES_COLUMNS, null, null)) { + mPackages = null; + final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY).appendPath(TABLE_PACKAGES).appendPath("*") + .build(); + try (Cursor cursor = context.getContentResolver().query(providerUri, + TABLE_PACKAGES_COLUMNS, null, null)) { if (cursor == null) { - Log.w(TAG, "didn't get cursor"); + Log.w(TAG, "Didn't get cursor for " + providerUri); return; } final int count = cursor.getCount(); if (count == 0) { - Log.d(TAG, "no packages"); + if (DEBUG) { + Log.d(TAG, "No packages anymore (was " + mPackages + ")"); + } return; } mPackages = new ArraySet<>(count); while (cursor.moveToNext()) { mPackages.add(cursor.getString(TABLE_PACKAGES_COL_PACKAGE)); } - Log.v(TAG, "init(): " + mPackages); + if (DEBUG) { + Log.d(TAG, "init(): " + mPackages); + } } } + @Override public boolean filterApp(AppEntry info) { return mPackages != null && mPackages.contains(info.info.packageName); diff --git a/src/com/android/settings/applications/DirectoryAccessDetails.java b/src/com/android/settings/applications/DirectoryAccessDetails.java index 1f7a81a9336..43422d0f90b 100644 --- a/src/com/android/settings/applications/DirectoryAccessDetails.java +++ b/src/com/android/settings/applications/DirectoryAccessDetails.java @@ -17,84 +17,236 @@ package com.android.settings.applications; import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_DIRECTORY; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_PACKAGE; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_VOLUME_UUID; import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS; import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS; import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED; import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE; import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID; +import static com.android.settings.applications.AppStateDirectoryAccessBridge.DEBUG; +import static com.android.settings.applications.AppStateDirectoryAccessBridge.VERBOSE; + +import android.annotation.Nullable; +import android.app.Activity; import android.app.AlertDialog; import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.os.storage.StorageManager; +import android.os.storage.VolumeInfo; +import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.Preference.OnPreferenceChangeListener; import android.support.v7.preference.Preference.OnPreferenceClickListener; +import android.text.TextUtils; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.preference.PreferenceScreen; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IconDrawableFactory; import android.util.Log; +import android.util.Pair; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; +import com.android.settings.widget.EntityHeaderController; +import com.android.settings.widget.EntityHeaderController.ActionType; +import com.android.settingslib.applications.AppUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Detailed settings for an app's directory access permissions (A.K.A Scoped Directory Access). + * + *

Currently, it shows the entry for which the user denied access with the "Do not ask again" + * flag checked on: the user than can use the settings toggle to reset that deniel. + * + *

This fragments dynamically lists all such permissions, starting with one preference per + * directory in the primary storage, then adding additional entries for the external volumes (one + * entry for the whole volume). */ -// TODO(b/63720392): explain its layout -// TODO(b/63720392): add unit tests -public class DirectoryAccessDetails extends AppInfoWithHeader implements OnPreferenceChangeListener, - OnPreferenceClickListener { - private static final String MY_TAG = "DirectoryAccessDetails"; +// TODO(b/72055774): add unit tests +public class DirectoryAccessDetails extends AppInfoBase { + + @SuppressWarnings("hiding") + private static final String TAG = "DirectoryAccessDetails"; + + private boolean mCreated; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mCreated) { + Log.w(TAG, "onActivityCreated(): ignoring duplicate call"); + return; + } + mCreated = true; + if (mPackageInfo == null) { + Log.w(TAG, "onActivityCreated(): no package info"); + return; + } + final Activity activity = getActivity(); + final Preference pref = EntityHeaderController + .newInstance(activity, this, /* header= */ null ) + .setRecyclerView(getListView(), getLifecycle()) + .setIcon(IconDrawableFactory.newInstance(getPrefContext()) + .getBadgedIcon(mPackageInfo.applicationInfo)) + .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm)) + .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo)) + .setPackageName(mPackageName) + .setUid(mPackageInfo.applicationInfo.uid) + .setHasAppInfoLink(false) + .setButtonActions(ActionType.ACTION_NONE, ActionType.ACTION_NONE) + .done(activity, getPrefContext()); + getPreferenceScreen().addPreference(pref); + } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (true) { - // TODO(b/63720392): temporarily hack so the screen doesn't crash.. - addPreferencesFromResource(R.xml.app_ops_permissions_details); - // ... we need to dynamically create the preferences by calling the provider instead: - try (Cursor cursor = getContentResolver().query( - new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*") - .build(), - TABLE_PERMISSIONS_COLUMNS, null, null)) { - if (cursor == null) { - Log.w(TAG, "didn't get cursor"); - return; + final Context context = getPrefContext(); + addPreferencesFromResource(R.xml.directory_access_details); + final PreferenceScreen prefsGroup = getPreferenceScreen(); + + // Set external directory UUIDs. + ArraySet externalDirectoryUuids = null; + + final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*") + .build(); + // Query provider for entries. + try (Cursor cursor = context.getContentResolver().query(providerUri, + TABLE_PERMISSIONS_COLUMNS, null, new String[] { mPackageName }, null)) { + if (cursor == null) { + Log.w(TAG, "Didn't get cursor for " + mPackageName); + return; + } + final int count = cursor.getCount(); + if (count == 0) { + if (DEBUG) { + Log.d(TAG, "No permissions for " + mPackageName); } - final int count = cursor.getCount(); - if (count == 0) { - Log.d(TAG, "no permissions"); - return; + // TODO(b/72055774): display empty message + return; + } + + while (cursor.moveToNext()) { + final String pkg = cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE); + final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID); + final String dir = cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY); + if (VERBOSE) { + Log.v(TAG, "Pkg:" + pkg + " uuid: " + uuid + " dir: " + dir); } - while (cursor.moveToNext()) { - final String pkg = cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE); - final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID); - final String dir = cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY); - final boolean granted = cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1; - Log.v(MY_TAG, "pkg:" + pkg + " uuid: " + uuid + " dir: " + dir + "> " - + granted); + + if (!mPackageName.equals(pkg)) { + // Sanity check, shouldn't happen + Log.w(TAG, "Ignoring " + uuid + "/" + dir + " due to package mismatch: " + + "expected " + mPackageName + ", got " + pkg); + continue; + } + + if (uuid == null) { + // Primary storage entry: add right away + prefsGroup.addPreference( + newPreference(context, dir, providerUri, /* uuid= */ null, dir)); + } else { + // External volume entry: save it for later. + if (externalDirectoryUuids == null) { + externalDirectoryUuids = new ArraySet<>(1); + } + externalDirectoryUuids.add(uuid); } } } + + // Add entries from external volumes + if (externalDirectoryUuids != null) { + if (VERBOSE) { + Log.v(TAG, "adding external directories: " + externalDirectoryUuids); + } + + // Query StorageManager to get the user-friendly volume names. + final StorageManager sm = context.getSystemService(StorageManager.class); + final List volumes = sm.getVolumes(); + if (volumes.isEmpty()) { + Log.w(TAG, "StorageManager returned no secondary volumes"); + return; + } + final Map volumeNames = new HashMap<>(volumes.size()); + for (VolumeInfo volume : volumes) { + final String uuid = volume.getFsUuid(); + if (uuid == null) continue; // Primary storage; not used. + + String name = sm.getBestVolumeDescription(volume); + if (name == null) { + Log.w(TAG, "No description for " + volume + "; using uuid instead: " + uuid); + name = uuid; + } + volumeNames.put(uuid, name); + } + if (VERBOSE) { + Log.v(TAG, "UUID -> name mapping: " + volumeNames); + } + + externalDirectoryUuids.forEach((uuid) ->{ + final String name = volumeNames.get(uuid); + // TODO(b/72055774): add separator + prefsGroup.addPreference( + newPreference(context, name, providerUri, uuid, /* dir= */ null)); + }); + } + return; + } + + + private SwitchPreference newPreference(Context context, String title, Uri providerUri, + String uuid, String dir) { + final SwitchPreference pref = new SwitchPreference(context); + pref.setKey(String.format("%s:%s", uuid, dir)); + pref.setTitle(title); + pref.setChecked(false); + pref.setOnPreferenceChangeListener((unused, value) -> { + resetDoNotAskAgain(context, value, providerUri, uuid, dir); + return true; + }); + return pref; + } + + private void resetDoNotAskAgain(Context context, Object value, Uri providerUri, + @Nullable String uuid, @Nullable String directory) { + if (!Boolean.class.isInstance(value)) { + // Sanity check + Log.wtf(TAG, "Invalid value from switch: " + value); + return; + } + final boolean newValue = ((Boolean) value).booleanValue(); + if (DEBUG) { + Log.d(TAG, "Asking " + providerUri + " to update " + uuid + "/" + directory + " to " + + newValue); + } + final ContentValues values = new ContentValues(1); + values.put(COL_GRANTED, newValue); + final int updated = context.getContentResolver().update(providerUri, values, + null, new String[] { mPackageName, uuid, directory }); + if (DEBUG) { + Log.d(TAG, "Updated " + updated + " entries for " + uuid + "/" + directory); + } } - @Override - public boolean onPreferenceClick(Preference preference) { - // TODO(b/63720392): implement or remove listener - return false; - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - // TODO(b/63720392): implement or remove listener - return false; - } - @Override protected boolean refreshUi() { - // TODO(b/63720392): implement return true; }