Loading res/xml/directory_access_details.xml 0 → 100644 +20 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2018 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. --> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:settings="http://schemas.android.com/apk/res-auto" android:key="directory_access_details" android:title="@string/directory_access"/> res/xml/special_access.xml +1 −3 Original line number Diff line number Diff line Loading @@ -111,7 +111,6 @@ android:value="com.android.settings.Settings$VrListenersSettingsActivity" /> </Preference> <!-- TODO(b/63720392): add when ready <Preference android:key="special_app_directory_access" android:title="@string/directory_access" Loading @@ -119,8 +118,7 @@ settings:keywords="@string/keywords_directory_access"> <extra android:name="classname" android:value="com.android.settings.Settings$DirectoryeAccessSettingsActivity" /> android:value="com.android.settings.Settings$DirectoryAccessSettingsActivity" /> </Preference> --> </PreferenceScreen> src/com/android/settings/applications/AppStateDirectoryAccessBridge.java +19 −8 Original line number Diff line number Diff line Loading @@ -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); } Loading @@ -59,26 +63,33 @@ public class AppStateDirectoryAccessBridge extends AppStateBaseBridge { @Override public void init(Context context) { try (Cursor cursor = context.getContentResolver().query( new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) mPackages = null; final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY).appendPath(TABLE_PACKAGES).appendPath("*") .build(), TABLE_PACKAGES_COLUMNS, null, null)) { .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) { Loading src/com/android/settings/applications/DirectoryAccessDetails.java +190 −38 Original line number Diff line number Diff line Loading @@ -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). * * <p>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. * * <p>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) final Context context = getPrefContext(); addPreferencesFromResource(R.xml.directory_access_details); final PreferenceScreen prefsGroup = getPreferenceScreen(); // Set external directory UUIDs. ArraySet<String> externalDirectoryUuids = null; final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*") .build(), TABLE_PERMISSIONS_COLUMNS, null, null)) { .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"); Log.w(TAG, "Didn't get cursor for " + mPackageName); return; } final int count = cursor.getCount(); if (count == 0) { Log.d(TAG, "no permissions"); if (DEBUG) { Log.d(TAG, "No permissions for " + mPackageName); } // 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); final boolean granted = cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1; Log.v(MY_TAG, "pkg:" + pkg + " uuid: " + uuid + " dir: " + dir + "> " + granted); if (VERBOSE) { Log.v(TAG, "Pkg:" + pkg + " uuid: " + uuid + " dir: " + dir); } 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); } } } @Override public boolean onPreferenceClick(Preference preference) { // TODO(b/63720392): implement or remove listener return false; // Add entries from external volumes if (externalDirectoryUuids != null) { if (VERBOSE) { Log.v(TAG, "adding external directories: " + externalDirectoryUuids); } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { // TODO(b/63720392): implement or remove listener return false; // Query StorageManager to get the user-friendly volume names. final StorageManager sm = context.getSystemService(StorageManager.class); final List<VolumeInfo> volumes = sm.getVolumes(); if (volumes.isEmpty()) { Log.w(TAG, "StorageManager returned no secondary volumes"); return; } final Map<String, String> 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 protected boolean refreshUi() { // TODO(b/63720392): implement return true; } Loading Loading
res/xml/directory_access_details.xml 0 → 100644 +20 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2018 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. --> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:settings="http://schemas.android.com/apk/res-auto" android:key="directory_access_details" android:title="@string/directory_access"/>
res/xml/special_access.xml +1 −3 Original line number Diff line number Diff line Loading @@ -111,7 +111,6 @@ android:value="com.android.settings.Settings$VrListenersSettingsActivity" /> </Preference> <!-- TODO(b/63720392): add when ready <Preference android:key="special_app_directory_access" android:title="@string/directory_access" Loading @@ -119,8 +118,7 @@ settings:keywords="@string/keywords_directory_access"> <extra android:name="classname" android:value="com.android.settings.Settings$DirectoryeAccessSettingsActivity" /> android:value="com.android.settings.Settings$DirectoryAccessSettingsActivity" /> </Preference> --> </PreferenceScreen>
src/com/android/settings/applications/AppStateDirectoryAccessBridge.java +19 −8 Original line number Diff line number Diff line Loading @@ -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); } Loading @@ -59,26 +63,33 @@ public class AppStateDirectoryAccessBridge extends AppStateBaseBridge { @Override public void init(Context context) { try (Cursor cursor = context.getContentResolver().query( new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) mPackages = null; final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY).appendPath(TABLE_PACKAGES).appendPath("*") .build(), TABLE_PACKAGES_COLUMNS, null, null)) { .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) { Loading
src/com/android/settings/applications/DirectoryAccessDetails.java +190 −38 Original line number Diff line number Diff line Loading @@ -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). * * <p>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. * * <p>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) final Context context = getPrefContext(); addPreferencesFromResource(R.xml.directory_access_details); final PreferenceScreen prefsGroup = getPreferenceScreen(); // Set external directory UUIDs. ArraySet<String> externalDirectoryUuids = null; final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*") .build(), TABLE_PERMISSIONS_COLUMNS, null, null)) { .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"); Log.w(TAG, "Didn't get cursor for " + mPackageName); return; } final int count = cursor.getCount(); if (count == 0) { Log.d(TAG, "no permissions"); if (DEBUG) { Log.d(TAG, "No permissions for " + mPackageName); } // 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); final boolean granted = cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1; Log.v(MY_TAG, "pkg:" + pkg + " uuid: " + uuid + " dir: " + dir + "> " + granted); if (VERBOSE) { Log.v(TAG, "Pkg:" + pkg + " uuid: " + uuid + " dir: " + dir); } 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); } } } @Override public boolean onPreferenceClick(Preference preference) { // TODO(b/63720392): implement or remove listener return false; // Add entries from external volumes if (externalDirectoryUuids != null) { if (VERBOSE) { Log.v(TAG, "adding external directories: " + externalDirectoryUuids); } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { // TODO(b/63720392): implement or remove listener return false; // Query StorageManager to get the user-friendly volume names. final StorageManager sm = context.getSystemService(StorageManager.class); final List<VolumeInfo> volumes = sm.getVolumes(); if (volumes.isEmpty()) { Log.w(TAG, "StorageManager returned no secondary volumes"); return; } final Map<String, String> 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 protected boolean refreshUi() { // TODO(b/63720392): implement return true; } Loading