summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/values/strings.xml5
-rw-r--r--res/xml/security_settings_misc.xml5
-rw-r--r--res/xml/usage_access_settings.xml19
-rw-r--r--src/com/android/settings/UsageAccessSettings.java314
4 files changed, 343 insertions, 0 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 92d8420..02511b3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4328,6 +4328,11 @@
<!-- Description of dialog to explain that a lock screen password is required to use credential storage [CHAR LIMIT=NONE] -->
<string name="credentials_configure_lock_screen_hint">You need to set a lock screen PIN or password before you can use credential storage.</string>
+ <!-- Title of Usage Access preference item [CHAR LIMIT=30] -->
+ <string name="usage_access_title">Usage access</string>
+ <!-- Summary of Usage Access preference item [CHAR LIMIT=80] -->
+ <string name="usage_access_summary">Apps that have access to your device\'s usage history.</string>
+
<!-- Sound settings screen, setting check box label -->
<string name="emergency_tone_title">Emergency tone</string>
<!-- Sound settings screen, setting option summary text -->
diff --git a/res/xml/security_settings_misc.xml b/res/xml/security_settings_misc.xml
index 1fa8b9a..06743a5 100644
--- a/res/xml/security_settings_misc.xml
+++ b/res/xml/security_settings_misc.xml
@@ -120,6 +120,11 @@
android:summary="@string/switch_off_text"
android:fragment="com.android.settings.ScreenPinningSettings"/>
+ <Preference android:key="usage_access"
+ android:title="@string/usage_access_title"
+ android:summary="@string/usage_access_summary"
+ android:fragment="com.android.settings.UsageAccessSettings"/>
+
</PreferenceCategory>
</PreferenceScreen>
diff --git a/res/xml/usage_access_settings.xml b/res/xml/usage_access_settings.xml
new file mode 100644
index 0000000..9cea725
--- /dev/null
+++ b/res/xml/usage_access_settings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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:key="usage_access"
+ android:title="@string/usage_access_title" />
diff --git a/src/com/android/settings/UsageAccessSettings.java b/src/com/android/settings/UsageAccessSettings.java
new file mode 100644
index 0000000..8e8e533
--- /dev/null
+++ b/src/com/android/settings/UsageAccessSettings.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2014 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;
+
+import com.android.internal.content.PackageMonitor;
+
+import android.Manifest;
+import android.app.ActivityThread;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.util.List;
+
+public class UsageAccessSettings extends SettingsPreferenceFragment implements
+ Preference.OnPreferenceChangeListener {
+
+ private static final String TAG = "UsageAccessSettings";
+
+ private static final String[] PM_USAGE_STATS_PERMISSION = new String[] {
+ Manifest.permission.PACKAGE_USAGE_STATS
+ };
+
+ private static final int[] APP_OPS_OP_CODES = new int[] {
+ AppOpsManager.OP_GET_USAGE_STATS
+ };
+
+ private static class PackageEntry {
+ public PackageEntry(String packageName) {
+ this.packageName = packageName;
+ this.appOpMode = AppOpsManager.MODE_DEFAULT;
+ }
+
+ final String packageName;
+ PackageInfo packageInfo;
+ boolean permissionGranted;
+ int appOpMode;
+
+ SwitchPreference preference;
+ }
+
+ /**
+ * Fetches the list of Apps that are requesting access to the UsageStats API and updates
+ * the PreferenceScreen with the results when complete.
+ */
+ private class AppsRequestingAccessFetcher extends
+ AsyncTask<Void, Void, ArrayMap<String, PackageEntry>> {
+
+ private final Context mContext;
+ private final PackageManager mPackageManager;
+ private final IPackageManager mIPackageManager;
+
+ public AppsRequestingAccessFetcher(Context context) {
+ mContext = context;
+ mPackageManager = context.getPackageManager();
+ mIPackageManager = ActivityThread.getPackageManager();
+ }
+
+ @Override
+ protected ArrayMap<String, PackageEntry> doInBackground(Void... params) {
+ final String[] packages;
+ try {
+ packages = mIPackageManager.getAppOpPermissionPackages(
+ Manifest.permission.PACKAGE_USAGE_STATS);
+ } catch (RemoteException e) {
+ Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting "
+ + Manifest.permission.PACKAGE_USAGE_STATS);
+ return null;
+ }
+
+ if (packages == null) {
+ // No packages are requesting permission to use the UsageStats API.
+ return null;
+ }
+
+ ArrayMap<String, PackageEntry> entries = new ArrayMap<>();
+ for (final String packageName : packages) {
+ if (!shouldIgnorePackage(packageName)) {
+ entries.put(packageName, new PackageEntry(packageName));
+ }
+ }
+
+ // Load the packages that have been granted the PACKAGE_USAGE_STATS permission.
+ final List<PackageInfo> packageInfos = mPackageManager.getPackagesHoldingPermissions(
+ PM_USAGE_STATS_PERMISSION, 0);
+ final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0;
+ for (int i = 0; i < packageInfoCount; i++) {
+ final PackageInfo packageInfo = packageInfos.get(i);
+ final PackageEntry pe = entries.get(packageInfo.packageName);
+ if (pe != null) {
+ pe.packageInfo = packageInfo;
+ pe.permissionGranted = true;
+ }
+ }
+
+ // Load the remaining packages that have requested but don't have the
+ // PACKAGE_USAGE_STATS permission.
+ int packageCount = entries.size();
+ for (int i = 0; i < packageCount; i++) {
+ final PackageEntry pe = entries.valueAt(i);
+ if (pe.packageInfo == null) {
+ try {
+ pe.packageInfo = mPackageManager.getPackageInfo(pe.packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // This package doesn't exist. This may occur when an app is uninstalled for
+ // one user, but it is not removed from the system.
+ entries.removeAt(i);
+ i--;
+ packageCount--;
+ }
+ }
+ }
+
+ // Find out which packages have been granted permission from AppOps.
+ final List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps(
+ APP_OPS_OP_CODES);
+ final int packageOpsCount = packageOps != null ? packageOps.size() : 0;
+ for (int i = 0; i < packageOpsCount; i++) {
+ final AppOpsManager.PackageOps packageOp = packageOps.get(i);
+ final PackageEntry pe = entries.get(packageOp.getPackageName());
+ if (pe == null) {
+ Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName()
+ + " but package doesn't exist or did not request UsageStats access");
+ continue;
+ }
+
+ if (packageOp.getOps().size() < 1) {
+ Log.w(TAG, "No AppOps permission exists for package "
+ + packageOp.getPackageName());
+ continue;
+ }
+
+ pe.appOpMode = packageOp.getOps().get(0).getMode();
+ }
+
+ return entries;
+ }
+
+ @Override
+ protected void onPostExecute(ArrayMap<String, PackageEntry> newEntries) {
+ mLastFetcherTask = null;
+
+ if (getActivity() == null) {
+ // We must have finished the Activity while we were processing in the background.
+ return;
+ }
+
+ if (newEntries == null) {
+ mPackageEntryMap.clear();
+ getPreferenceScreen().removeAll();
+ return;
+ }
+
+ // Find the deleted entries and remove them from the PreferenceScreen.
+ final int oldPackageCount = mPackageEntryMap.size();
+ for (int i = 0; i < oldPackageCount; i++) {
+ final PackageEntry oldPackageEntry = mPackageEntryMap.valueAt(i);
+ final PackageEntry newPackageEntry = newEntries.get(oldPackageEntry.packageName);
+ if (newPackageEntry == null) {
+ // This package has been removed.
+ getPreferenceScreen().removePreference(oldPackageEntry.preference);
+ } else {
+ // This package already exists in the preference hierarchy, so reuse that
+ // Preference.
+ newPackageEntry.preference = oldPackageEntry.preference;
+ }
+ }
+
+ // Now add new packages to the PreferenceScreen.
+ final int packageCount = newEntries.size();
+ for (int i = 0; i < packageCount; i++) {
+ final PackageEntry packageEntry = newEntries.valueAt(i);
+ if (packageEntry.preference == null) {
+ packageEntry.preference = new SwitchPreference(mContext);
+ packageEntry.preference.setPersistent(false);
+ packageEntry.preference.setOnPreferenceChangeListener(UsageAccessSettings.this);
+ getPreferenceScreen().addPreference(packageEntry.preference);
+ }
+ updatePreference(packageEntry);
+ }
+
+ mPackageEntryMap.clear();
+ mPackageEntryMap = newEntries;
+ }
+
+ private void updatePreference(PackageEntry pe) {
+ pe.preference.setIcon(pe.packageInfo.applicationInfo.loadIcon(mPackageManager));
+ pe.preference.setTitle(pe.packageInfo.applicationInfo.loadLabel(mPackageManager));
+ pe.preference.setKey(pe.packageName);
+
+ boolean check = false;
+ if (pe.appOpMode == AppOpsManager.MODE_ALLOWED) {
+ check = true;
+ } else if (pe.appOpMode == AppOpsManager.MODE_DEFAULT) {
+ // If the default AppOps mode is set, then fall back to
+ // whether the app has been granted permission by PackageManager.
+ check = pe.permissionGranted;
+ }
+
+ if (check != pe.preference.isChecked()) {
+ pe.preference.setChecked(check);
+ }
+ }
+ }
+
+ private static boolean shouldIgnorePackage(String packageName) {
+ return packageName.equals("android") || packageName.equals("com.android.settings");
+ }
+
+ private ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>();
+ private AppOpsManager mAppOpsManager;
+ private AppsRequestingAccessFetcher mLastFetcherTask;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.usage_access_settings);
+ getPreferenceScreen().setOrderingAsAdded(false);
+ mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ updateInterestedApps();
+ mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ mPackageMonitor.unregister();
+ if (mLastFetcherTask != null) {
+ mLastFetcherTask.cancel(true);
+ mLastFetcherTask = null;
+ }
+ }
+
+ private void updateInterestedApps() {
+ if (mLastFetcherTask != null) {
+ // Canceling can only fail for some obscure reason since mLastFetcherTask would be
+ // null if the task has already completed. So we ignore the result of cancel and
+ // spawn a new task to get fresh data. AsyncTask executes tasks serially anyways,
+ // so we are safe from running two tasks at the same time.
+ mLastFetcherTask.cancel(true);
+ }
+
+ mLastFetcherTask = new AppsRequestingAccessFetcher(getActivity());
+ mLastFetcherTask.execute();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ final String packageName = preference.getKey();
+ final PackageEntry pe = mPackageEntryMap.get(packageName);
+ if (pe == null) {
+ Log.w(TAG, "Preference change event for package " + packageName
+ + " but that package is no longer valid.");
+ return false;
+ }
+
+ if (!(newValue instanceof Boolean)) {
+ Log.w(TAG, "Preference change event for package " + packageName
+ + " had non boolean value of type " + newValue.getClass().getName());
+ return false;
+ }
+
+ final int newMode = (Boolean) newValue ?
+ AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
+ mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS, pe.packageInfo.applicationInfo.uid,
+ packageName, newMode);
+ pe.appOpMode = newMode;
+ return true;
+ }
+
+ private final PackageMonitor mPackageMonitor = new PackageMonitor() {
+ @Override
+ public void onPackageAdded(String packageName, int uid) {
+ updateInterestedApps();
+ }
+
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ updateInterestedApps();
+ }
+ };
+}