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
This commit is contained in:
Felipe Leme
2018-01-19 17:34:23 -08:00
parent 578d0e8ed4
commit 6d88d79aa5
4 changed files with 234 additions and 53 deletions

View File

@@ -0,0 +1,20 @@
<?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"/>

View File

@@ -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"
@@ -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>

View File

@@ -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);

View File

@@ -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)
.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<String> 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<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
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;
}