diff --git a/res/values/strings.xml b/res/values/strings.xml index 46411e6f09e..cfefa55cc45 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9174,4 +9174,9 @@ Track all GNSS constellations and frequencies with no duty cycling + + Storage access + + storage access scoped directory + diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml index 829bc53c6c1..9980eb60bc6 100644 --- a/res/xml/special_access.xml +++ b/res/xml/special_access.xml @@ -111,4 +111,16 @@ android:value="com.android.settings.Settings$VrListenersSettingsActivity" /> + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 505977d9686..0269bb2de4b 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -120,6 +120,7 @@ public class Settings extends SettingsActivity { public static class PhotosStorageActivity extends SettingsActivity { /* empty */ } + public static class StorageAccessSettingsActivity extends SettingsActivity { /* empty */ } public static class TopLevelSettings extends SettingsActivity { /* empty */ } public static class ApnSettingsActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/applications/AppStateStorageAccessBridge.java b/src/com/android/settings/applications/AppStateStorageAccessBridge.java new file mode 100644 index 00000000000..4839fd6baab --- /dev/null +++ b/src/com/android/settings/applications/AppStateStorageAccessBridge.java @@ -0,0 +1,88 @@ +/* + * 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. + */ +package com.android.settings.applications; + +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS; +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.ArraySet; +import android.util.Log; + +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; + +import java.util.Set; + +// TODO(b/63720392): add unit tests +public class AppStateStorageAccessBridge extends AppStateBaseBridge { + + private static final String TAG = "StorageAccessBridge"; + + public AppStateStorageAccessBridge(ApplicationsState appState, Callback callback) { + super(appState, callback); + } + + @Override + protected void loadAllExtraInfo() { } + + @Override + protected void updateExtraInfo(AppEntry app, String pkg, int uid) { } + + public static final AppFilter FILTER_APP_HAS_STORAGE_ACCESS = new AppFilter() { + + private Set mPackages; + + @Override + public void init() { + throw new UnsupportedOperationException("Need to call constructor that takes context"); + } + + @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)) { + if (cursor == null) { + Log.w(TAG, "didn't get cursor"); + return; + } + final int count = cursor.getCount(); + if (count == 0) { + Log.d(TAG, "no packages"); + return; + } + mPackages = new ArraySet<>(count); + while (cursor.moveToNext()) { + mPackages.add(cursor.getString(TABLE_PACKAGES_COL_PACKAGE)); + } + Log.v(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/StorageAccessDetails.java b/src/com/android/settings/applications/StorageAccessDetails.java new file mode 100644 index 00000000000..41729e664c5 --- /dev/null +++ b/src/com/android/settings/applications/StorageAccessDetails.java @@ -0,0 +1,110 @@ +/* + * 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. + */ + +package com.android.settings.applications; + +import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY; +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 android.app.AlertDialog; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceChangeListener; +import android.support.v7.preference.Preference.OnPreferenceClickListener; +import android.util.Log; + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.R; + +/** + * Detailed settings for an app's storage access permissions (A.K.A Scoped Directory Access). + */ +// TODO(b/63720392): explain its layout +// TODO(b/63720392): add unit tests +public class StorageAccessDetails extends AppInfoWithHeader implements OnPreferenceChangeListener, + OnPreferenceClickListener { + private static final String MY_TAG = "StorageAccessDetails"; + + @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 int count = cursor.getCount(); + if (count == 0) { + Log.d(TAG, "no permissions"); + 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); + } + } + } + } + + @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; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.APPLICATIONS_USAGE_ACCESS_DETAIL; + } +} diff --git a/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java b/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java index 01d2cb83f5d..82b3d86a70e 100644 --- a/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java +++ b/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java @@ -23,6 +23,7 @@ import com.android.settings.applications.AppStateInstallAppsBridge; import com.android.settings.applications.AppStateNotificationBridge; import com.android.settings.applications.AppStateOverlayBridge; import com.android.settings.applications.AppStatePowerBridge; +import com.android.settings.applications.AppStateStorageAccessBridge; import com.android.settings.applications.AppStateUsageBridge; import com.android.settings.applications.AppStateWriteSettingsBridge; import com.android.settingslib.applications.ApplicationsState; @@ -65,14 +66,15 @@ public class AppFilterRegistry { public static final int FILTER_APPS_WITH_OVERLAY = 10; public static final int FILTER_APPS_WRITE_SETTINGS = 11; public static final int FILTER_APPS_INSTALL_SOURCES = 12; - // Next id: 13 + public static final int FILTER_APP_HAS_STORAGE_ACCESS = 13; + // Next id: 14 private static AppFilterRegistry sRegistry; private final AppFilterItem[] mFilters; private AppFilterRegistry() { - mFilters = new AppFilterItem[13]; + mFilters = new AppFilterItem[14]; // High power whitelist, on mFilters[FILTER_APPS_POWER_WHITELIST] = new AppFilterItem( @@ -155,6 +157,12 @@ public class AppFilterRegistry { AppStateInstallAppsBridge.FILTER_APP_SOURCES, FILTER_APPS_INSTALL_SOURCES, R.string.filter_install_sources_apps); + + // Apps that interacted with storage access permissions (A.K.A. Scoped Directory Access) + mFilters[FILTER_APP_HAS_STORAGE_ACCESS] = new AppFilterItem( + AppStateStorageAccessBridge.FILTER_APP_HAS_STORAGE_ACCESS, + FILTER_APP_HAS_STORAGE_ACCESS, + R.string.filter_install_sources_apps); } public static AppFilterRegistry getInstance() { @@ -177,6 +185,8 @@ public class AppFilterRegistry { return FILTER_APPS_WRITE_SETTINGS; case ManageApplications.LIST_TYPE_MANAGE_SOURCES: return FILTER_APPS_INSTALL_SOURCES; + case ManageApplications.LIST_TYPE_STORAGE_ACCESS: + return FILTER_APP_HAS_STORAGE_ACCESS; default: return FILTER_APPS_ALL; } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index dcdd3b8e487..081161e2da1 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -84,6 +84,7 @@ import com.android.settings.applications.AppStateInstallAppsBridge; import com.android.settings.applications.AppStateNotificationBridge; import com.android.settings.applications.AppStateOverlayBridge; import com.android.settings.applications.AppStatePowerBridge; +import com.android.settings.applications.AppStateStorageAccessBridge; import com.android.settings.applications.AppStateUsageBridge; import com.android.settings.applications.AppStateUsageBridge.UsageState; import com.android.settings.applications.AppStateWriteSettingsBridge; @@ -92,6 +93,7 @@ import com.android.settings.applications.DefaultAppSettings; import com.android.settings.applications.InstalledAppCounter; import com.android.settings.applications.InstalledAppDetails; import com.android.settings.applications.NotificationApps; +import com.android.settings.applications.StorageAccessDetails; import com.android.settings.applications.UsageAccessDetails; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.applications.appinfo.DrawOverlayDetails; @@ -204,6 +206,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment public static final int LIST_TYPE_GAMES = 9; public static final int LIST_TYPE_MOVIES = 10; public static final int LIST_TYPE_PHOTOGRAPHY = 11; + public static final int LIST_TYPE_STORAGE_ACCESS = 12; // List types that should show instant apps. public static final Set LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList( @@ -279,6 +282,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment mListType = LIST_TYPE_PHOTOGRAPHY; mSortOrder = R.id.sort_order_size; mStorageType = args.getInt(EXTRA_STORAGE_TYPE, STORAGE_TYPE_DEFAULT); + } else if (className.equals(Settings.StorageAccessSettingsActivity.class.getName())) { + mListType = LIST_TYPE_STORAGE_ACCESS; + screenTitle = R.string.storage_access; } else { mListType = LIST_TYPE_MAIN; } @@ -443,6 +449,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS; case LIST_TYPE_MANAGE_SOURCES: return MetricsEvent.MANAGE_EXTERNAL_SOURCES; + case LIST_TYPE_STORAGE_ACCESS: + return MetricsEvent.STORAGE_ACCESS; default: return MetricsEvent.VIEW_UNKNOWN; } @@ -537,6 +545,10 @@ public class ManageApplications extends InstrumentedPreferenceFragment case LIST_TYPE_PHOTOGRAPHY: startAppInfoFragment(AppStorageSettings.class, R.string.storage_photos_videos); break; + case LIST_TYPE_STORAGE_ACCESS: + startAppInfoFragment(StorageAccessDetails.class, R.string.storage_access); + break; + // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed // app. Maybe when they load the list of apps that contains managed profile apps. @@ -840,6 +852,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment mExtraInfoBridge = new AppStateWriteSettingsBridge(mContext, mState, this); } else if (mManageApplications.mListType == LIST_TYPE_MANAGE_SOURCES) { mExtraInfoBridge = new AppStateInstallAppsBridge(mContext, mState, this); + } else if (mManageApplications.mListType == LIST_TYPE_STORAGE_ACCESS) { + mExtraInfoBridge = new AppStateStorageAccessBridge(mState, this); } else { mExtraInfoBridge = null; } @@ -1241,6 +1255,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment case LIST_TYPE_MANAGE_SOURCES: holder.setSummary(ExternalSourcesDetails.getPreferenceSummary(mContext, entry)); break; + case LIST_TYPE_STORAGE_ACCESS: + holder.setSummary(null); + break; default: holder.updateSizeText(entry, mManageApplications.mInvalidSizeStr, mWhichSize); break; diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 44f576c737e..71ff610b7f9 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -44,6 +44,7 @@ import com.android.settings.applications.ManageDomainUrls; import com.android.settings.applications.NotificationApps; import com.android.settings.applications.ProcessStatsSummary; import com.android.settings.applications.ProcessStatsUi; +import com.android.settings.applications.StorageAccessDetails; import com.android.settings.applications.UsageAccessDetails; import com.android.settings.applications.VrListenerSettings; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; @@ -252,7 +253,8 @@ public class SettingsGateway { WebViewAppPicker.class.getName(), LockscreenDashboardFragment.class.getName(), BluetoothDeviceDetailsFragment.class.getName(), - DataUsageList.class.getName() + DataUsageList.class.getName(), + StorageAccessDetails.class.getName() }; public static final String[] SETTINGS_FOR_RESTRICTED = { diff --git a/tests/robotests/assets/grandfather_not_implementing_indexable b/tests/robotests/assets/grandfather_not_implementing_indexable index a82c9efc996..eb06125b3b8 100644 --- a/tests/robotests/assets/grandfather_not_implementing_indexable +++ b/tests/robotests/assets/grandfather_not_implementing_indexable @@ -72,4 +72,5 @@ com.android.settings.IccLockSettings com.android.settings.TetherSettings com.android.settings.ApnEditor com.android.settings.UserCredentialsSettings -com.android.settings.TestingSettings \ No newline at end of file +com.android.settings.TestingSettings +com.android.settings.applications.StorageAccessDetails \ No newline at end of file