Implement the Usage Access Settings UI
Scans for apps requesting permission to use the UsageStats API and displays them so that the user can grant or deny access. Bug: 16461070 Change-Id: Ie9f611688581cdd60ffe9f59e566f658ac2564e9
This commit is contained in:
@@ -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 -->
|
||||
|
@@ -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>
|
||||
|
19
res/xml/usage_access_settings.xml
Normal file
19
res/xml/usage_access_settings.xml
Normal file
@@ -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" />
|
314
src/com/android/settings/UsageAccessSettings.java
Normal file
314
src/com/android/settings/UsageAccessSettings.java
Normal file
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user