Add Usage Access control
Bug: 63672290
Test: Settings -> Apps -> Usage Access
Change-Id: I0cc24e2040cf6307e55913720871b0b573fcea53
(cherry picked from commit db8bb1b704d51343b09e24a9c03d8129d5272767)
(cherry picked from commit 6e908bc33779e9113d240993212238640cc5e61f)
diff --git a/Settings/res/values/strings.xml b/Settings/res/values/strings.xml
index 5f8d40c..a546dd0 100644
--- a/Settings/res/values/strings.xml
+++ b/Settings/res/values/strings.xml
@@ -1451,4 +1451,15 @@
<string name="available_virtual_keyboard_category">Available virtual keyboards</string>
<!-- Title for the preference entry for managing keyboards [CHAR LIMIT=50] -->
<string name="manage_keyboards">Manage keyboards</string>
+
+ <!-- Preference summary text for an app when it is allowed for a permission. [CHAR LIMIT=45] -->
+ <string name="app_permission_summary_allowed">Allowed</string>
+ <!-- Preference summary text for an app when it is disallowed for a permission. [CHAR LIMIT=45] -->
+ <string name="app_permission_summary_not_allowed">Not allowed</string>
+
+ <!-- Title of usage access screen [CHAR LIMIT=30] -->
+ <string name="usage_access">Usage access</string>
+ <!-- Description of the usage access setting [CHAR LIMIT=NONE] -->
+ <string name="usage_access_description">Usage access allows an app to track what other apps you\u2019re using and how often, as well as your carrier, language settings, and other details.</string>
+
</resources>
diff --git a/Settings/res/xml/app_usage_access.xml b/Settings/res/xml/app_usage_access.xml
new file mode 100644
index 0000000..6b1c461
--- /dev/null
+++ b/Settings/res/xml/app_usage_access.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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"
+ android:title="@string/usage_access">
+ <Preference
+ android:key="header"
+ android:title="@string/usage_access_description"
+ android:singleLineTitle="false"/>
+</PreferenceScreen>
diff --git a/Settings/res/xml/apps.xml b/Settings/res/xml/apps.xml
new file mode 100644
index 0000000..e462b55
--- /dev/null
+++ b/Settings/res/xml/apps.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 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"
+ android:title="@string/device_apps">
+ <Preference
+ android:key="Permissions"
+ android:title="@string/device_apps_permissions">
+ <intent android:action="android.intent.action.MANAGE_PERMISSIONS"/>
+ </Preference>
+ <Preference
+ android:key="AppUsage"
+ android:title="@string/usage_access"
+ android:fragment="com.android.tv.settings.device.apps.specialaccess.ManageAppUsageAccess"/>
+ <PreferenceCategory
+ android:key="DownloadedPreferenceGroup"
+ android:title="@string/apps_downloaded"
+ android:orderingFromXml="false"/>
+ <PreferenceCategory
+ android:key="SystemPreferenceGroup"
+ android:title="@string/apps_system"
+ android:orderingFromXml="false"/>
+</PreferenceScreen>
diff --git a/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java b/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java
index 50bd405..cd799e0 100644
--- a/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java
+++ b/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java
@@ -16,8 +16,6 @@
package com.android.tv.settings.device.apps;
-import android.content.Context;
-import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.os.Handler;
@@ -25,9 +23,7 @@
import android.support.annotation.NonNull;
import android.support.v17.preference.LeanbackPreferenceFragment;
import android.support.v7.preference.Preference;
-import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceGroup;
-import android.support.v7.preference.PreferenceScreen;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -135,37 +131,19 @@
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
- final Context context = getPreferenceManager().getContext();
-
- final PreferenceScreen screen =
- getPreferenceManager().createPreferenceScreen(context);
- screen.setTitle(R.string.device_apps);
// TODO: show volume name somewhere?
+ setPreferencesFromResource(R.xml.apps, null);
+
final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID);
- if (TextUtils.isEmpty(volumeUuid)) {
- final Preference permissionsPreference = new Preference(context);
- permissionsPreference.setKey("Permissions");
- permissionsPreference.setTitle(R.string.device_apps_permissions);
- permissionsPreference.setIntent(new Intent(Intent.ACTION_MANAGE_PERMISSIONS));
- screen.addPreference(permissionsPreference);
- }
- mDownloadedPreferenceGroup = new PreferenceCategory(context);
- mDownloadedPreferenceGroup.setKey("DownloadedPreferenceGroup");
- mDownloadedPreferenceGroup.setTitle(R.string.apps_downloaded);
- screen.addPreference(mDownloadedPreferenceGroup);
- mDownloadedPreferenceGroup.setOrderingAsAdded(false);
+ final Preference permissionPreference = findPreference("Permissions");
+ permissionPreference.setVisible(TextUtils.isEmpty(volumeUuid));
- if (TextUtils.isEmpty(volumeUuid)) {
- mSystemPreferenceGroup = new PreferenceCategory(context);
- mSystemPreferenceGroup.setKey("SystemPreferenceGroup");
- mSystemPreferenceGroup.setTitle(R.string.apps_system);
- screen.addPreference(mSystemPreferenceGroup);
- mSystemPreferenceGroup.setOrderingAsAdded(false);
- }
+ mDownloadedPreferenceGroup = (PreferenceGroup) findPreference("DownloadedPreferenceGroup");
- setPreferenceScreen(screen);
+ mSystemPreferenceGroup = (PreferenceGroup) findPreference("SystemPreferenceGroup");
+ mSystemPreferenceGroup.setVisible(TextUtils.isEmpty(volumeUuid));
}
@Override
diff --git a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppOp.java b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppOp.java
new file mode 100644
index 0000000..cb98004
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppOp.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2017 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.tv.settings.device.apps.specialaccess;
+
+import android.app.ActivityThread;
+import android.app.AppOpsManager;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settingslib.applications.ApplicationsState;
+
+/**
+ * Base class for managing app ops
+ */
+public abstract class ManageAppOp extends ManageApplications {
+ private static final String TAG = "ManageAppOps";
+
+ private IPackageManager mIPackageManager;
+ private AppOpsManager mAppOpsManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mIPackageManager = ActivityThread.getPackageManager();
+ mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+ super.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public ApplicationsState.AppFilter getFilter() {
+ return new ApplicationsState.AppFilter() {
+ @Override
+ public void init() {
+ }
+
+ @Override
+ public boolean filterApp(ApplicationsState.AppEntry entry) {
+ entry.extraInfo = createPermissionStateFor(entry.info.packageName, entry.info.uid);
+ return !shouldIgnorePackage(entry.info.packageName)
+ && ((PermissionState) entry.extraInfo).isPermissible();
+ }
+ };
+ }
+
+ /**
+ * @return AppOps code
+ */
+ public abstract int getAppOpsOpCode();
+
+ /**
+ * @return Manifest permission string
+ */
+ public abstract String getPermission();
+
+ private boolean hasRequestedAppOpPermission(String permission, String packageName) {
+ try {
+ String[] packages = mIPackageManager.getAppOpPermissionPackages(permission);
+ return ArrayUtils.contains(packages, packageName);
+ } catch (RemoteException exc) {
+ Log.e(TAG, "PackageManager dead. Cannot get permission info");
+ return false;
+ }
+ }
+
+ private boolean hasPermission(int uid) {
+ try {
+ int result = mIPackageManager.checkUidPermission(getPermission(), uid);
+ return result == PackageManager.PERMISSION_GRANTED;
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManager dead. Cannot get permission info");
+ return false;
+ }
+ }
+
+ private int getAppOpMode(int uid, String packageName) {
+ return mAppOpsManager.checkOpNoThrow(getAppOpsOpCode(), uid, packageName);
+ }
+
+ private PermissionState createPermissionStateFor(String packageName, int uid) {
+ return new PermissionState(
+ hasRequestedAppOpPermission(getPermission(), packageName),
+ hasPermission(uid),
+ getAppOpMode(uid, packageName));
+ }
+
+ /*
+ * Checks for packages that should be ignored for further processing
+ */
+ private boolean shouldIgnorePackage(String packageName) {
+ return packageName.equals("android") || packageName.equals(getContext().getPackageName());
+ }
+
+ /**
+ * Collection of information to be used as {@link ApplicationsState.AppEntry#extraInfo} objects
+ */
+ protected static class PermissionState {
+ public final boolean permissionRequested;
+ public final boolean permissionGranted;
+ public final int appOpMode;
+
+ private PermissionState(boolean permissionRequested, boolean permissionGranted,
+ int appOpMode) {
+ this.permissionRequested = permissionRequested;
+ this.permissionGranted = permissionGranted;
+ this.appOpMode = appOpMode;
+ }
+
+ /**
+ * @return True if the permission is granted
+ */
+ public boolean isAllowed() {
+ if (appOpMode == AppOpsManager.MODE_DEFAULT) {
+ return permissionGranted;
+ } else {
+ return appOpMode == AppOpsManager.MODE_ALLOWED;
+ }
+ }
+
+ /**
+ * @return True if the permission is relevant
+ */
+ public boolean isPermissible() {
+ return appOpMode != AppOpsManager.MODE_DEFAULT || permissionRequested;
+ }
+
+ @Override
+ public String toString() {
+ return "[permissionGranted: " + permissionGranted
+ + ", permissionRequested: " + permissionRequested
+ + ", appOpMode: " + appOpMode
+ + "]";
+ }
+ }
+}
diff --git a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppUsageAccess.java b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppUsageAccess.java
new file mode 100644
index 0000000..ae4da3b
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppUsageAccess.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 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.tv.settings.device.apps.specialaccess;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.os.Bundle;
+import android.support.annotation.Keep;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.TwoStatePreference;
+
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.tv.settings.R;
+
+import java.util.Comparator;
+
+/**
+ * Fragment for controlling if apps can monitor app usage
+ */
+@Keep
+public class ManageAppUsageAccess extends ManageAppOp {
+
+ private AppOpsManager mAppOpsManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+ }
+
+ @Override
+ public int getAppOpsOpCode() {
+ return AppOpsManager.OP_GET_USAGE_STATS;
+ }
+
+ @Override
+ public String getPermission() {
+ return Manifest.permission.PACKAGE_USAGE_STATS;
+ }
+
+ @Nullable
+ @Override
+ public Comparator<ApplicationsState.AppEntry> getComparator() {
+ return ApplicationsState.ALPHA_COMPARATOR;
+ }
+
+ @NonNull
+ @Override
+ public Preference bindPreference(@NonNull Preference preference,
+ ApplicationsState.AppEntry entry) {
+ final TwoStatePreference switchPref = (SwitchPreference) preference;
+ switchPref.setTitle(entry.label);
+ switchPref.setKey(entry.info.packageName);
+ mApplicationsState.ensureIcon(entry);
+ switchPref.setIcon(entry.icon);
+ switchPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ setAppUsageAccess(entry, (Boolean) newValue);
+ return true;
+ });
+
+ switchPref.setSummary(getPreferenceSummary(entry));
+ switchPref.setChecked(((PermissionState) entry.extraInfo).isAllowed());
+
+ return switchPref;
+ }
+
+ @NonNull
+ @Override
+ public Preference createAppPreference() {
+ return new SwitchPreference(getPreferenceManager().getContext());
+ }
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.app_usage_access, null);
+ }
+
+ private CharSequence getPreferenceSummary(ApplicationsState.AppEntry entry) {
+ if (entry.extraInfo instanceof PermissionState) {
+ return getContext().getText(((PermissionState) entry.extraInfo).isPermissible()
+ ? R.string.app_permission_summary_allowed
+ : R.string.app_permission_summary_not_allowed);
+ } else {
+ return null;
+ }
+ }
+
+ private void setAppUsageAccess(ApplicationsState.AppEntry entry, boolean grant) {
+ mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS,
+ entry.info.uid, entry.info.packageName,
+ grant ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+ updateAppList();
+ }
+}
diff --git a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java
index 049a7ea..6709328 100644
--- a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java
+++ b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java
@@ -17,12 +17,8 @@
package com.android.tv.settings.device.apps.specialaccess;
import android.Manifest;
-import android.app.ActivityThread;
import android.app.AppOpsManager;
-import android.content.pm.IPackageManager;
-import android.content.pm.PackageManager;
import android.os.Bundle;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.support.annotation.Keep;
@@ -31,9 +27,7 @@
import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.TwoStatePreference;
-import android.util.Log;
-import com.android.internal.util.ArrayUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.tv.settings.R;
@@ -43,37 +37,23 @@
* Fragment for controlling if apps can install other apps
*/
@Keep
-public class ManageExternalSources extends ManageApplications {
- private static final String TAG = "ManageExternalSources";
-
- private IPackageManager mIpm;
+public class ManageExternalSources extends ManageAppOp {
private AppOpsManager mAppOpsManager;
@Override
public void onCreate(Bundle savedInstanceState) {
- mIpm = ActivityThread.getPackageManager();
mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
super.onCreate(savedInstanceState);
}
- @NonNull
@Override
- public ApplicationsState.AppFilter getFilter() {
- return new ApplicationsState.AppFilter() {
+ public int getAppOpsOpCode() {
+ return AppOpsManager.OP_REQUEST_INSTALL_PACKAGES;
+ }
- @Override
- public void init() {
- }
-
- @Override
- public boolean filterApp(ApplicationsState.AppEntry entry) {
- if (!(entry.extraInfo instanceof InstallAppsState)) {
- loadInstallAppsState(entry);
- }
- InstallAppsState state = (InstallAppsState) entry.extraInfo;
- return state.isPotentialAppSource();
- }
- };
+ @Override
+ public String getPermission() {
+ return Manifest.permission.REQUEST_INSTALL_PACKAGES;
}
@Nullable
@@ -107,9 +87,8 @@
return true;
});
- loadInstallAppsState(entry);
- InstallAppsState state = (InstallAppsState) entry.extraInfo;
- switchPref.setChecked(state.canInstallApps());
+ PermissionState state = (PermissionState) entry.extraInfo;
+ switchPref.setChecked(state.isAllowed());
switchPref.setSummary(getPreferenceSummary(entry));
switchPref.setEnabled(canChange(entry));
return switchPref;
@@ -143,14 +122,8 @@
return getContext().getString(R.string.disabled);
}
- final InstallAppsState appsState;
- if (entry.extraInfo instanceof InstallAppsState) {
- appsState = (InstallAppsState) entry.extraInfo;
- } else {
- entry.extraInfo = appsState =
- createInstallAppsStateFor(entry.info.packageName, entry.info.uid);
- }
- return getContext().getString(appsState.canInstallApps() ? R.string.external_source_trusted
+ return getContext().getString(((PermissionState) entry.extraInfo).isAllowed()
+ ? R.string.external_source_trusted
: R.string.external_source_untrusted);
}
@@ -160,77 +133,4 @@
newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED);
updateAppList();
}
-
-
- private boolean hasRequestedAppOpPermission(String permission, String packageName) {
- try {
- String[] packages = mIpm.getAppOpPermissionPackages(permission);
- return ArrayUtils.contains(packages, packageName);
- } catch (RemoteException exc) {
- Log.e(TAG, "PackageManager dead. Cannot get permission info");
- return false;
- }
- }
-
- private boolean hasPermission(String permission, int uid) {
- try {
- int result = mIpm.checkUidPermission(permission, uid);
- return result == PackageManager.PERMISSION_GRANTED;
- } catch (RemoteException e) {
- Log.e(TAG, "PackageManager dead. Cannot get permission info");
- return false;
- }
- }
-
- private int getAppOpMode(int appOpCode, int uid, String packageName) {
- return mAppOpsManager.checkOpNoThrow(appOpCode, uid, packageName);
- }
-
- private void loadInstallAppsState(ApplicationsState.AppEntry entry) {
- entry.extraInfo = createInstallAppsStateFor(entry.info.packageName, entry.info.uid);
- }
-
- private InstallAppsState createInstallAppsStateFor(String packageName, int uid) {
- final InstallAppsState appState = new InstallAppsState();
- appState.permissionRequested = hasRequestedAppOpPermission(
- Manifest.permission.REQUEST_INSTALL_PACKAGES, packageName);
- appState.permissionGranted = hasPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES,
- uid);
- appState.appOpMode = getAppOpMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, uid,
- packageName);
- return appState;
- }
-
- /**
- * Collection of information to be used as {@link ApplicationsState.AppEntry#extraInfo} objects
- */
- private static class InstallAppsState {
- public boolean permissionRequested;
- public boolean permissionGranted;
- public int appOpMode;
-
- private InstallAppsState() {
- this.appOpMode = AppOpsManager.MODE_DEFAULT;
- }
-
- public boolean canInstallApps() {
- if (appOpMode == AppOpsManager.MODE_DEFAULT) {
- return permissionGranted;
- } else {
- return appOpMode == AppOpsManager.MODE_ALLOWED;
- }
- }
-
- public boolean isPotentialAppSource() {
- return appOpMode != AppOpsManager.MODE_DEFAULT || permissionRequested;
- }
-
- @Override
- public String toString() {
- return "[permissionGranted: " + permissionGranted
- + ", permissionRequested: " + permissionRequested
- + ", appOpMode: " + appOpMode
- + "]";
- }
- }
}