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;
}