New UX for app usage screen
Now uses ManageApplications base, and has a details screen which has
a switch and a link to optional app settings.
Bug: 20290386
Change-Id: If32ce8d82e55f3908644c575925b3f6506a68e6e
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f47ef0d..aec34bc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1228,7 +1228,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
- android:value="com.android.settings.UsageAccessSettings" />
+ android:value="com.android.settings.applications.ManageApplications" />
<meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/security_settings" />
</activity>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 72adf4e..080cd3e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4497,10 +4497,6 @@
<!-- Title of Usage Access preference item [CHAR LIMIT=30] -->
<string name="usage_access_title">Apps with usage access</string>
- <!-- AlertDialog title for warning user when enabling usage access [CHAR LIMIT=30] -->
- <string name="allow_usage_access_title">Allow access?</string>
- <!-- AlertDialog message for warning user when enabling usage access [CHAR LIMIT=NONE] -->
- <string name="allow_usage_access_message">If you allow access, this app can view general information about the apps on your device, such as how often you use them.</string>
<!-- Sound settings screen, setting check box label -->
<string name="emergency_tone_title">Emergency tone</string>
@@ -6461,4 +6457,16 @@
<!-- Title of app storage screen [CHAR LIMIT=30] -->
<string name="apps_storage">Apps storage</string>
+ <!-- Title of usage access screen [CHAR LIMIT=30] -->
+ <string name="usage_access">Usage access</string>
+
+ <!-- Label for setting which controls whether app has usage access [CHAR LIMIT=45] -->
+ <string name="permit_usage_access">Permit usage access</string>
+
+ <!-- Link to the apps page for app usage settings [CHAR LIMIT=45] -->
+ <string name="app_usage_preference">App usage preferences</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\'re using and how often, as well as your carrier, language settings, and other details.</string>
+
</resources>
diff --git a/res/xml/security_settings_misc.xml b/res/xml/security_settings_misc.xml
index 71f9ffa..b67e1b7 100644
--- a/res/xml/security_settings_misc.xml
+++ b/res/xml/security_settings_misc.xml
@@ -117,7 +117,11 @@
<Preference android:key="usage_access"
android:title="@string/usage_access_title"
- android:fragment="com.android.settings.UsageAccessSettings"/>
+ android:fragment="com.android.settings.applications.ManageApplications">
+ <extra
+ android:name="classname"
+ android:value="com.android.settings.Settings$UsageAccessSettingsActivity" />
+ </Preference>
</PreferenceCategory>
diff --git a/res/xml/usage_access_details.xml b/res/xml/usage_access_details.xml
new file mode 100644
index 0000000..d8b3bb1
--- /dev/null
+++ b/res/xml/usage_access_details.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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/com.android.settings"
+ android:title="@string/usage_access">
+
+ <SwitchPreference
+ android:key="usage_switch"
+ android:title="@string/permit_usage_access" />
+
+ <Preference
+ android:key="app_usage_preference"
+ android:title="@string/app_usage_preference" />
+
+ <Preference
+ android:summary="@string/usage_access_description"
+ android:selectable="false" />
+
+</PreferenceScreen>
diff --git a/src/com/android/settings/InstrumentedFragment.java b/src/com/android/settings/InstrumentedFragment.java
index 883c67c..d239d4e 100644
--- a/src/com/android/settings/InstrumentedFragment.java
+++ b/src/com/android/settings/InstrumentedFragment.java
@@ -28,6 +28,7 @@
public static final int VIEW_CATEGORY_DEFAULT_APPS = VIEW_CATEGORY_UNDECLARED + 1;
public static final int VIEW_CATEGORY_STORAGE_APPS = VIEW_CATEGORY_UNDECLARED + 2;
+ public static final int VIEW_CATEGORY_USAGE_ACCESS_DETAIL = VIEW_CATEGORY_UNDECLARED + 3;
/**
* Declare the view of this category.
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index e8be34a..caf2491 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -76,6 +76,7 @@
import com.android.settings.applications.InstalledAppDetails;
import com.android.settings.applications.ManageApplications;
import com.android.settings.applications.ProcessStatsUi;
+import com.android.settings.applications.UsageAccessDetails;
import com.android.settings.bluetooth.BluetoothSettings;
import com.android.settings.dashboard.DashboardCategory;
import com.android.settings.dashboard.DashboardSummary;
@@ -300,7 +301,7 @@
NotificationStation.class.getName(),
LocationSettings.class.getName(),
SecuritySettings.class.getName(),
- UsageAccessSettings.class.getName(),
+ UsageAccessDetails.class.getName(),
PrivacySettings.class.getName(),
DeviceAdminSettings.class.getName(),
AccessibilitySettings.class.getName(),
diff --git a/src/com/android/settings/UsageAccessSettings.java b/src/com/android/settings/UsageAccessSettings.java
deleted file mode 100644
index 704c3c5..0000000
--- a/src/com/android/settings/UsageAccessSettings.java
+++ /dev/null
@@ -1,569 +0,0 @@
-/*
- * 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.AlertDialog;
-import android.app.AppOpsManager;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.app.FragmentTransaction;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.pm.ApplicationInfo;
-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.os.UserHandle;
-import android.os.UserManager;
-import android.preference.Preference;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.SparseArray;
-import com.android.internal.logging.MetricsLogger;
-
-import java.util.List;
-import java.util.Collections;
-
-public class UsageAccessSettings extends SettingsPreferenceFragment implements
- Preference.OnPreferenceChangeListener {
-
- private static final String TAG = "UsageAccessSettings";
- private static final String BUNDLE_KEY_PROFILEID = "profileId";
-
- 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 implements Comparable<PackageEntry> {
- public PackageEntry(String packageName, UserHandle userHandle) {
- this.packageName = packageName;
- this.appOpMode = AppOpsManager.MODE_DEFAULT;
- this.userHandle = userHandle;
- }
-
- @Override
- public int compareTo(PackageEntry another) {
- return packageName.compareTo(another.packageName);
- }
-
- final String packageName;
- PackageInfo packageInfo;
- boolean permissionGranted;
- int appOpMode;
- UserHandle userHandle;
-
- 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, SparseArray<ArrayMap<String, PackageEntry>>> {
-
- private final Context mContext;
- private final PackageManager mPackageManager;
- private final IPackageManager mIPackageManager;
- private final UserManager mUserManager;
- private final List<UserHandle> mProfiles;
-
- public AppsRequestingAccessFetcher(Context context) {
- mContext = context;
- mPackageManager = context.getPackageManager();
- mIPackageManager = ActivityThread.getPackageManager();
- mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
- mProfiles = mUserManager.getUserProfiles();
- }
-
- @Override
- protected SparseArray<ArrayMap<String, PackageEntry>> doInBackground(Void... params) {
- final String[] packages;
- SparseArray<ArrayMap<String, PackageEntry>> entries;
- try {
- packages = mIPackageManager.getAppOpPermissionPackages(
- Manifest.permission.PACKAGE_USAGE_STATS);
-
- if (packages == null) {
- // No packages are requesting permission to use the UsageStats API.
- return null;
- }
-
- entries = new SparseArray<>();
- for (final UserHandle profile : mProfiles) {
- final ArrayMap<String, PackageEntry> entriesForProfile = new ArrayMap<>();
- final int profileId = profile.getIdentifier();
- entries.put(profileId, entriesForProfile);
- for (final String packageName : packages) {
- final boolean isAvailable = mIPackageManager.isPackageAvailable(packageName,
- profileId);
- if (!shouldIgnorePackage(packageName) && isAvailable) {
- final PackageEntry newEntry = new PackageEntry(packageName, profile);
- entriesForProfile.put(packageName, newEntry);
- }
- }
- }
- } catch (RemoteException e) {
- Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting "
- + Manifest.permission.PACKAGE_USAGE_STATS);
- return null;
- }
-
- // Load the packages that have been granted the PACKAGE_USAGE_STATS permission.
- try {
- for (final UserHandle profile : mProfiles) {
- final int profileId = profile.getIdentifier();
- final ArrayMap<String, PackageEntry> entriesForProfile = entries.get(profileId);
- if (entriesForProfile == null) {
- continue;
- }
- final List<PackageInfo> packageInfos = mIPackageManager
- .getPackagesHoldingPermissions(PM_USAGE_STATS_PERMISSION, 0, profileId)
- .getList();
- final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0;
- for (int i = 0; i < packageInfoCount; i++) {
- final PackageInfo packageInfo = packageInfos.get(i);
- final PackageEntry pe = entriesForProfile.get(packageInfo.packageName);
- if (pe != null) {
- pe.packageInfo = packageInfo;
- pe.permissionGranted = true;
- }
- }
- }
- } catch (RemoteException e) {
- Log.w(TAG, "PackageManager is dead. Can't get list of packages granted "
- + Manifest.permission.PACKAGE_USAGE_STATS);
- return null;
- }
-
- // Load the remaining packages that have requested but don't have the
- // PACKAGE_USAGE_STATS permission.
- for (final UserHandle profile : mProfiles) {
- final int profileId = profile.getIdentifier();
- final ArrayMap<String, PackageEntry> entriesForProfile = entries.get(profileId);
- if (entriesForProfile == null) {
- continue;
- }
- int packageCount = entriesForProfile.size();
- for (int i = packageCount - 1; i >= 0; --i) {
- final PackageEntry pe = entriesForProfile.valueAt(i);
- if (pe.packageInfo == null) {
- try {
- pe.packageInfo = mIPackageManager.getPackageInfo(pe.packageName, 0,
- profileId);
- } catch (RemoteException 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.
- entriesForProfile.removeAt(i);
- }
- }
- }
- }
-
- // 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 int userId = UserHandle.getUserId(packageOp.getUid());
- if (!isThisUserAProfileOfCurrentUser(userId)) {
- // This AppOp does not belong to any of this user's profiles.
- continue;
- }
-
- final ArrayMap<String, PackageEntry> entriesForProfile = entries.get(userId);
- if (entriesForProfile == null) {
- continue;
- }
- final PackageEntry pe = entriesForProfile.get(packageOp.getPackageName());
- if (pe == null) {
- Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName()
- + " of user " + userId +
- " 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(SparseArray<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();
- mPreferenceScreen.removeAll();
- return;
- }
-
- // Find the deleted entries and remove them from the PreferenceScreen.
- final int oldProfileCount = mPackageEntryMap.size();
- for (int profileIndex = 0; profileIndex < oldProfileCount; ++profileIndex) {
- final int profileId = mPackageEntryMap.keyAt(profileIndex);
- final ArrayMap<String, PackageEntry> oldEntriesForProfile = mPackageEntryMap
- .valueAt(profileIndex);
- final int oldPackageCount = oldEntriesForProfile.size();
-
- final ArrayMap<String, PackageEntry> newEntriesForProfile = newEntries.get(
- profileId);
-
- for (int i = 0; i < oldPackageCount; i++) {
- final PackageEntry oldPackageEntry = oldEntriesForProfile.valueAt(i);
-
- PackageEntry newPackageEntry = null;
- if (newEntriesForProfile != null) {
- newPackageEntry = newEntriesForProfile.get(oldPackageEntry.packageName);
- }
- if (newPackageEntry == null) {
- // This package has been removed.
- mPreferenceScreen.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 newProfileCount = newEntries.size();
- for (int profileIndex = 0; profileIndex < newProfileCount; ++profileIndex) {
- final int profileId = newEntries.keyAt(profileIndex);
- final ArrayMap<String, PackageEntry> newEntriesForProfile = newEntries.get(
- profileId);
- final int packageCount = newEntriesForProfile.size();
- for (int i = 0; i < packageCount; i++) {
- final PackageEntry packageEntry = newEntriesForProfile.valueAt(i);
- if (packageEntry.preference == null) {
- packageEntry.preference = new SwitchPreference(mContext);
- packageEntry.preference.setPersistent(false);
- packageEntry.preference.setOnPreferenceChangeListener(
- UsageAccessSettings.this);
- mPreferenceScreen.addPreference(packageEntry.preference);
- }
- updatePreference(packageEntry);
- }
- }
- mPackageEntryMap.clear();
- mPackageEntryMap = newEntries;
-
- // Add/remove headers if necessary. If there are package entries only for one user and
- // that user is not the managed profile then do not show headers.
- if (mPackageEntryMap.size() == 1 &&
- mPackageEntryMap.keyAt(0) == UserHandle.myUserId()) {
- for (int i = 0; i < mCategoryHeaders.length; ++i) {
- if (mCategoryHeaders[i] != null) {
- mPreferenceScreen.removePreference(mCategoryHeaders[i]);
- }
- mCategoryHeaders[i] = null;
- }
- } else {
- for (int i = 0; i < mCategoryHeaders.length; ++i) {
- if (mCategoryHeaders[i] == null) {
- final Preference preference = new Preference(mContext, null,
- com.android.internal.R.attr.preferenceCategoryStyle, 0);
- mCategoryHeaders[i] = preference;
- preference.setTitle(mCategoryHeaderTitleResIds[i]);
- preference.setEnabled(false);
- mPreferenceScreen.addPreference(preference);
- }
- }
- }
-
- // Sort preferences alphabetically within categories
- int order = 0;
- final int profileCount = mProfiles.size();
- for (int i = 0; i < profileCount; ++i) {
- Preference header = mCategoryHeaders[i];
- if (header != null) {
- header.setOrder(order++);
- }
- ArrayMap<String, PackageEntry> entriesForProfile =
- mPackageEntryMap.get(mProfiles.get(i).getIdentifier());
- if (entriesForProfile != null) {
- List<PackageEntry> sortedEntries = Collections.list(
- Collections.enumeration(entriesForProfile.values()));
- Collections.sort(sortedEntries);
- for (PackageEntry pe : sortedEntries) {
- pe.preference.setOrder(order++);
- }
- }
- }
- }
-
- private void updatePreference(PackageEntry pe) {
- final int profileId = pe.userHandle.getIdentifier();
- // Set something as default
- pe.preference.setEnabled(false);
- pe.preference.setTitle(pe.packageName);
- pe.preference.setIcon(mUserManager.getBadgedIconForUser(mPackageManager
- .getDefaultActivityIcon(), pe.userHandle));
- try {
- // Try setting real title and icon
- final ApplicationInfo info = mIPackageManager.getApplicationInfo(pe.packageName,
- 0 /* no flags */, profileId);
- if (info != null) {
- pe.preference.setEnabled(true);
- pe.preference.setTitle(info.loadLabel(mPackageManager).toString());
- pe.preference.setIcon(mUserManager.getBadgedIconForUser(info.loadIcon(
- mPackageManager), pe.userHandle));
- }
- } catch (RemoteException e) {
- Log.w(TAG, "PackageManager is dead. Can't get app info for package " +
- pe.packageName + " of user " + profileId);
- // Keep going to update other parts of the preference
- }
-
- pe.preference.setKey(pe.packageName);
- Bundle extra = pe.preference.getExtras();
- extra.putInt(BUNDLE_KEY_PROFILEID, profileId);
-
- 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 boolean isThisUserAProfileOfCurrentUser(final int userId) {
- final int profilesMax = mProfiles.size();
- for (int i = 0; i < profilesMax; ++i) {
- if (mProfiles.get(i).getIdentifier() == userId) {
- return true;
- }
- }
- return false;
- }
- }
-
- static boolean shouldIgnorePackage(String packageName) {
- return packageName.equals("android") || packageName.equals("com.android.settings");
- }
-
- private AppsRequestingAccessFetcher mLastFetcherTask;
- SparseArray<ArrayMap<String, PackageEntry>> mPackageEntryMap = new SparseArray<>();
- AppOpsManager mAppOpsManager;
- PreferenceScreen mPreferenceScreen;
- private Preference[] mCategoryHeaders = new Preference[2];
- private static int[] mCategoryHeaderTitleResIds = new int[] {
- R.string.category_personal,
- R.string.category_work
- };
-
- @Override
- protected int getMetricsCategory() {
- return MetricsLogger.USAGE_ACCESS;
- }
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- addPreferencesFromResource(R.xml.usage_access_settings);
- mPreferenceScreen = getPreferenceScreen();
- mPreferenceScreen.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 int profileId = preference.getExtras().getInt(BUNDLE_KEY_PROFILEID);
- final PackageEntry pe = getPackageEntry(packageName, profileId);
- if (pe == null) {
- Log.w(TAG, "Preference change event handling failed");
- return false;
- }
-
- if (!(newValue instanceof Boolean)) {
- Log.w(TAG, "Preference change event for package " + packageName + " of user " +
- profileId + " had non boolean value of type " + newValue.getClass().getName());
- return false;
- }
-
- final int newMode = (Boolean) newValue ?
- AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
-
- // Check if we need to do any work.
- if (pe.appOpMode != newMode) {
- if (newMode != AppOpsManager.MODE_ALLOWED) {
- // Turning off the setting has no warning.
- setNewMode(pe, newMode);
- return true;
- }
-
- // Turning on the setting has a Warning.
- FragmentTransaction ft = getChildFragmentManager().beginTransaction();
- Fragment prev = getChildFragmentManager().findFragmentByTag("warning");
- if (prev != null) {
- ft.remove(prev);
- }
- WarningDialogFragment.newInstance(pe).show(ft, "warning");
- return false;
- }
- return true;
- }
-
- void setNewMode(PackageEntry pe, int newMode) {
- mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS,
- pe.packageInfo.applicationInfo.uid, pe.packageName, newMode);
- pe.appOpMode = newMode;
- }
-
- void allowAccess(String packageName, int profileId) {
- final PackageEntry entry = getPackageEntry(packageName, profileId);
- if (entry == null) {
- Log.w(TAG, "Unable to give access");
- return;
- }
-
- setNewMode(entry, AppOpsManager.MODE_ALLOWED);
- entry.preference.setChecked(true);
- }
-
- private PackageEntry getPackageEntry(String packageName, int profileId) {
- ArrayMap<String, PackageEntry> entriesForProfile = mPackageEntryMap.get(profileId);
- if (entriesForProfile == null) {
- Log.w(TAG, "getPackageEntry fails for package " + packageName + " of user " +
- profileId + ": user does not seem to be valid.");
- return null;
- }
- final PackageEntry entry = entriesForProfile.get(packageName);
- if (entry == null) {
- Log.w(TAG, "getPackageEntry fails for package " + packageName + " of user " +
- profileId + ": package does not exist.");
- }
- return entry;
- }
-
- private final PackageMonitor mPackageMonitor = new PackageMonitor() {
- @Override
- public void onPackageAdded(String packageName, int uid) {
- updateInterestedApps();
- }
-
- @Override
- public void onPackageRemoved(String packageName, int uid) {
- updateInterestedApps();
- }
- };
-
- public static class WarningDialogFragment extends DialogFragment
- implements DialogInterface.OnClickListener {
- private static final String ARG_PACKAGE_NAME = "package";
- private static final String ARG_PROFILE_ID = "profileId";
-
- public static WarningDialogFragment newInstance(PackageEntry pe) {
- WarningDialogFragment dialog = new WarningDialogFragment();
- Bundle args = new Bundle();
- args.putString(ARG_PACKAGE_NAME, pe.packageName);
- args.putInt(ARG_PROFILE_ID, pe.userHandle.getIdentifier());
- dialog.setArguments(args);
- return dialog;
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new AlertDialog.Builder(getActivity())
- .setTitle(R.string.allow_usage_access_title)
- .setMessage(R.string.allow_usage_access_message)
- .setIconAttribute(android.R.attr.alertDialogIcon)
- .setNegativeButton(R.string.cancel, this)
- .setPositiveButton(android.R.string.ok, this)
- .create();
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- ((UsageAccessSettings) getParentFragment()).allowAccess(
- getArguments().getString(ARG_PACKAGE_NAME),
- getArguments().getInt(ARG_PROFILE_ID));
- } else {
- dialog.cancel();
- }
- }
- }
-}
diff --git a/src/com/android/settings/applications/AppInfoBase.java b/src/com/android/settings/applications/AppInfoBase.java
index a7dc500..56fe885 100644
--- a/src/com/android/settings/applications/AppInfoBase.java
+++ b/src/com/android/settings/applications/AppInfoBase.java
@@ -32,7 +32,6 @@
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
-import android.preference.PreferenceFragment;
import android.util.Log;
import com.android.settings.InstrumentedPreferenceFragment;
diff --git a/src/com/android/settings/applications/AppStateBaseBridge.java b/src/com/android/settings/applications/AppStateBaseBridge.java
new file mode 100644
index 0000000..04eb77b
--- /dev/null
+++ b/src/com/android/settings/applications/AppStateBaseBridge.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2015 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 android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.android.settings.applications.ApplicationsState.AppEntry;
+import com.android.settings.applications.ApplicationsState.Session;
+
+import java.util.ArrayList;
+
+/**
+ * Common base class for bridging information to ApplicationsState.
+ */
+public abstract class AppStateBaseBridge implements ApplicationsState.Callbacks {
+
+ protected final ApplicationsState mAppState;
+ protected final Session mAppSession;
+ protected final Callback mCallback;
+ protected final BackgroundHandler mHandler;
+ protected final MainHandler mMainHandler;
+
+ public AppStateBaseBridge(ApplicationsState appState, Callback callback) {
+ mAppState = appState;
+ mAppSession = mAppState != null ? mAppState.newSession(this) : null;
+ mCallback = callback;
+ // Running on the same background thread as the ApplicationsState lets
+ // us run in the background and make sure they aren't doing updates at
+ // the same time as us as well.
+ mHandler = new BackgroundHandler(mAppState.getBackgroundLooper());
+ mMainHandler = new MainHandler();
+ }
+
+ public void resume() {
+ mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
+ mAppSession.resume();
+ }
+
+ public void pause() {
+ mAppSession.pause();
+ }
+
+ public void release() {
+ mAppSession.release();
+ }
+
+ public void forceUpdate(String pkg, int uid) {
+ mHandler.obtainMessage(BackgroundHandler.MSG_FORCE_LOAD_PKG, uid, 0, pkg).sendToTarget();
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
+ }
+
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ // No op.
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList<AppEntry> apps) {
+ // No op.
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ // No op.
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ // No op.
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ // No op.
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ // No op.
+ }
+
+ protected abstract void loadAllExtraInfo();
+ protected abstract void updateExtraInfo(AppEntry app, String pkg, int uid);
+
+ private class MainHandler extends Handler {
+ private static final int MSG_INFO_UPDATED = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INFO_UPDATED:
+ mCallback.onExtraInfoUpdated();
+ break;
+ }
+ }
+ }
+
+ private class BackgroundHandler extends Handler {
+ private static final int MSG_LOAD_ALL = 1;
+ private static final int MSG_FORCE_LOAD_PKG = 2;
+
+ public BackgroundHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LOAD_ALL:
+ loadAllExtraInfo();
+ mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+ break;
+ case MSG_FORCE_LOAD_PKG:
+ ArrayList<AppEntry> apps = mAppSession.getAllApps();
+ final int N = apps.size();
+ String pkg = (String) msg.obj;
+ int uid = msg.arg1;
+ for (int i = 0; i < N; i++) {
+ AppEntry app = apps.get(i);
+ if (app.info.uid == uid && pkg.equals(app.info.packageName)) {
+ updateExtraInfo(app, pkg, uid);
+ }
+ }
+ mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+ break;
+ }
+ }
+ }
+
+
+ public interface Callback {
+ void onExtraInfoUpdated();
+ }
+}
diff --git a/src/com/android/settings/applications/AppStateNotificationBridge.java b/src/com/android/settings/applications/AppStateNotificationBridge.java
index 0434c0e..1aa7ebf 100644
--- a/src/com/android/settings/applications/AppStateNotificationBridge.java
+++ b/src/com/android/settings/applications/AppStateNotificationBridge.java
@@ -16,156 +16,43 @@
package com.android.settings.applications;
import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
import com.android.settings.applications.ApplicationsState.AppEntry;
import com.android.settings.applications.ApplicationsState.AppFilter;
-import com.android.settings.applications.ApplicationsState.Session;
import com.android.settings.notification.NotificationBackend;
import com.android.settings.notification.NotificationBackend.AppRow;
import java.util.ArrayList;
-import java.util.List;
/**
* Connects the info provided by ApplicationsState and the NotificationBackend.
* Also provides app filters that can use the notification data.
*/
-public class AppStateNotificationBridge implements ApplicationsState.Callbacks {
+public class AppStateNotificationBridge extends AppStateBaseBridge {
- private final ApplicationsState mAppState;
private final NotificationBackend mNotifBackend;
- private final Session mAppSession;
- private final Callback mCallback;
- private final BackgroundHandler mHandler;
- private final MainHandler mMainHandler;
private final PackageManager mPm;
public AppStateNotificationBridge(PackageManager pm, ApplicationsState appState,
- NotificationBackend notifBackend, Callback callback) {
- mAppState = appState;
+ Callback callback, NotificationBackend notifBackend) {
+ super(appState, callback);
mPm = pm;
- mAppSession = mAppState.newSession(this);
mNotifBackend = notifBackend;
- mCallback = callback;
- // Running on the same background thread as the ApplicationsState lets
- // us run in the background and make sure they aren't doing updates at
- // the same time as us as well.
- mHandler = new BackgroundHandler(mAppState.getBackgroundLooper());
- mMainHandler = new MainHandler();
- mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
- }
-
- public void resume() {
- mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
- mAppSession.resume();
- }
-
- public void pause() {
- mAppSession.pause();
- }
-
- public void release() {
- mAppSession.release();
- }
-
- public void forceUpdate(String pkg, int uid) {
- mHandler.obtainMessage(BackgroundHandler.MSG_FORCE_LOAD_PKG, uid, 0, pkg).sendToTarget();
}
@Override
- public void onPackageListChanged() {
- mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
- }
-
- @Override
- public void onLoadEntriesCompleted() {
- mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
- }
-
- @Override
- public void onRunningStateChanged(boolean running) {
- // No op.
- }
-
- @Override
- public void onRebuildComplete(ArrayList<AppEntry> apps) {
- // No op.
- }
-
- @Override
- public void onPackageIconChanged() {
- // No op.
- }
-
- @Override
- public void onPackageSizeChanged(String packageName) {
- // No op.
- }
-
- @Override
- public void onAllSizesComputed() {
- // No op.
- }
-
- @Override
- public void onLauncherInfoChanged() {
- // No op.
- }
-
- private class MainHandler extends Handler {
- private static final int MSG_NOTIF_UPDATED = 1;
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_NOTIF_UPDATED:
- mCallback.onNotificationInfoUpdated();
- break;
- }
+ protected void loadAllExtraInfo() {
+ ArrayList<AppEntry> apps = mAppSession.getAllApps();
+ final int N = apps.size();
+ for (int i = 0; i < N; i++) {
+ AppEntry app = apps.get(i);
+ app.extraInfo = mNotifBackend.loadAppRow(mPm, app.info);
}
}
- private class BackgroundHandler extends Handler {
- private static final int MSG_LOAD_ALL = 1;
- private static final int MSG_FORCE_LOAD_PKG = 2;
-
- public BackgroundHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- List<AppEntry> apps = mAppSession.getAllApps();
- final int N = apps.size();
- switch (msg.what) {
- case MSG_LOAD_ALL:
- for (int i = 0; i < N; i++) {
- AppEntry app = apps.get(i);
- app.extraInfo = mNotifBackend.loadAppRow(mPm, app.info);
- }
- mMainHandler.sendEmptyMessage(MainHandler.MSG_NOTIF_UPDATED);
- break;
- case MSG_FORCE_LOAD_PKG:
- String pkg = (String) msg.obj;
- int uid = msg.arg1;
- for (int i = 0; i < N; i++) {
- AppEntry app = apps.get(i);
- if (app.info.uid == uid && pkg.equals(app.info.packageName)) {
- app.extraInfo = mNotifBackend.loadAppRow(mPm, app.info);
- break;
- }
- }
- mMainHandler.sendEmptyMessage(MainHandler.MSG_NOTIF_UPDATED);
- break;
- }
- }
- }
-
- public interface Callback {
- void onNotificationInfoUpdated();
+ @Override
+ protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
+ app.extraInfo = mNotifBackend.loadAppRow(mPm, app.info);
}
public static final AppFilter FILTER_APP_NOTIFICATION_BLOCKED = new AppFilter() {
diff --git a/src/com/android/settings/applications/AppStateUsageBridge.java b/src/com/android/settings/applications/AppStateUsageBridge.java
new file mode 100644
index 0000000..22f19b2
--- /dev/null
+++ b/src/com/android/settings/applications/AppStateUsageBridge.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2015 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 android.Manifest;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.PackageOps;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.settings.applications.ApplicationsState.AppEntry;
+import com.android.settings.applications.ApplicationsState.AppFilter;
+
+import java.util.List;
+
+/*
+ * Connects app usage info to the ApplicationsState.
+ * Also provides app filters that can use the info.
+ */
+public class AppStateUsageBridge extends AppStateBaseBridge {
+
+ private static final String TAG = "AppStateUsageBridge";
+
+ private static final String[] PM_USAGE_STATS_PERMISSION = {
+ Manifest.permission.PACKAGE_USAGE_STATS
+ };
+
+ private static final int[] APP_OPS_OP_CODES = {
+ AppOpsManager.OP_GET_USAGE_STATS
+ };
+
+ private final IPackageManager mIPackageManager;
+ private final UserManager mUserManager;
+ private final List<UserHandle> mProfiles;
+ private final AppOpsManager mAppOpsManager;
+ private final Context mContext;
+
+ public AppStateUsageBridge(Context context, ApplicationsState appState, Callback callback) {
+ super(appState, callback);
+ mContext = context;
+ mIPackageManager = AppGlobals.getPackageManager();
+ mUserManager = UserManager.get(context);
+ mProfiles = mUserManager.getUserProfiles();
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ }
+
+ private boolean isThisUserAProfileOfCurrentUser(final int userId) {
+ final int profilesMax = mProfiles.size();
+ for (int i = 0; i < profilesMax; i++) {
+ if (mProfiles.get(i).getIdentifier() == userId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
+ app.extraInfo = getUsageInfo(pkg, uid);
+ }
+
+ public UsageState getUsageInfo(String pkg, int uid) {
+ UsageState usageState = new UsageState(pkg, new UserHandle(UserHandle.getUserId(uid)));
+ try {
+ usageState.packageInfo = mIPackageManager.getPackageInfo(pkg,
+ PackageManager.GET_PERMISSIONS, usageState.userHandle.getIdentifier());
+ // Check permission state.
+ String[] requestedPermissions = usageState.packageInfo.requestedPermissions;
+ int[] permissionFlags = usageState.packageInfo.requestedPermissionsFlags;
+ if (requestedPermissions != null) {
+ for (int i = 0; i < requestedPermissions.length; i++) {
+ if (Manifest.permission.PACKAGE_USAGE_STATS.equals(requestedPermissions[i])
+ && (permissionFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED)
+ != 0) {
+ usageState.permissionGranted = true;
+ break;
+ }
+ }
+ }
+ // Check app op state.
+ List<PackageOps> ops = mAppOpsManager.getOpsForPackage(uid, pkg, APP_OPS_OP_CODES);
+ if (ops != null && ops.size() > 0 && ops.get(0).getOps().size() > 0) {
+ usageState.appOpMode = ops.get(0).getOps().get(0).getMode();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "PackageManager is dead. Can't get package info " + pkg, e);
+ }
+ return usageState;
+ }
+
+ @Override
+ protected void loadAllExtraInfo() {
+ SparseArray<ArrayMap<String, UsageState>> entries = getEntries();
+
+ // Load state info.
+ loadPermissionsStates(entries);
+ loadAppOpsStates(entries);
+
+ // Map states to application info.
+ List<AppEntry> apps = mAppSession.getAllApps();
+ final int N = apps.size();
+ for (int i = 0; i < N; i++) {
+ AppEntry app = apps.get(i);
+ int userId = UserHandle.getUserId(app.info.uid);
+ ArrayMap<String, UsageState> userMap = entries.get(userId);
+ app.extraInfo = userMap != null ? userMap.get(app.info.packageName) : null;
+ }
+ }
+
+ private SparseArray<ArrayMap<String, UsageState>> getEntries() {
+ try {
+ final String[] packages = mIPackageManager.getAppOpPermissionPackages(
+ Manifest.permission.PACKAGE_USAGE_STATS);
+
+ if (packages == null) {
+ // No packages are requesting permission to use the UsageStats API.
+ return null;
+ }
+
+ SparseArray<ArrayMap<String, UsageState>> entries = new SparseArray<>();
+ for (final UserHandle profile : mProfiles) {
+ final ArrayMap<String, UsageState> entriesForProfile = new ArrayMap<>();
+ final int profileId = profile.getIdentifier();
+ entries.put(profileId, entriesForProfile);
+ for (final String packageName : packages) {
+ final boolean isAvailable = mIPackageManager.isPackageAvailable(packageName,
+ profileId);
+ if (!shouldIgnorePackage(packageName) && isAvailable) {
+ final UsageState newEntry = new UsageState(packageName, profile);
+ entriesForProfile.put(packageName, newEntry);
+ }
+ }
+ }
+
+ return entries;
+ } catch (RemoteException e) {
+ Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting "
+ + Manifest.permission.PACKAGE_USAGE_STATS, e);
+ return null;
+ }
+ }
+
+ private void loadPermissionsStates(SparseArray<ArrayMap<String, UsageState>> entries) {
+ // Load the packages that have been granted the PACKAGE_USAGE_STATS permission.
+ try {
+ for (final UserHandle profile : mProfiles) {
+ final int profileId = profile.getIdentifier();
+ final ArrayMap<String, UsageState> entriesForProfile = entries.get(profileId);
+ if (entriesForProfile == null) {
+ continue;
+ }
+ @SuppressWarnings("unchecked")
+ final List<PackageInfo> packageInfos = mIPackageManager
+ .getPackagesHoldingPermissions(PM_USAGE_STATS_PERMISSION, 0, profileId)
+ .getList();
+ final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0;
+ for (int i = 0; i < packageInfoCount; i++) {
+ final PackageInfo packageInfo = packageInfos.get(i);
+ final UsageState pe = entriesForProfile.get(packageInfo.packageName);
+ if (pe != null) {
+ pe.packageInfo = packageInfo;
+ pe.permissionGranted = true;
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "PackageManager is dead. Can't get list of packages granted "
+ + Manifest.permission.PACKAGE_USAGE_STATS, e);
+ return;
+ }
+ }
+
+ private void loadAppOpsStates(SparseArray<ArrayMap<String, UsageState>> entries) {
+ // 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 int userId = UserHandle.getUserId(packageOp.getUid());
+ if (!isThisUserAProfileOfCurrentUser(userId)) {
+ // This AppOp does not belong to any of this user's profiles.
+ continue;
+ }
+
+ final ArrayMap<String, UsageState> entriesForProfile = entries.get(userId);
+ if (entriesForProfile == null) {
+ continue;
+ }
+ final UsageState pe = entriesForProfile.get(packageOp.getPackageName());
+ if (pe == null) {
+ Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName()
+ + " of user " + userId +
+ " 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();
+ }
+ }
+
+ private boolean shouldIgnorePackage(String packageName) {
+ return packageName.equals("android") || packageName.equals(mContext.getPackageName());
+ }
+
+ public static class UsageState {
+ public final String packageName;
+ public final UserHandle userHandle;
+ public PackageInfo packageInfo;
+ public boolean permissionGranted;
+ public int appOpMode;
+
+ public UsageState(String packageName, UserHandle userHandle) {
+ this.packageName = packageName;
+ this.appOpMode = AppOpsManager.MODE_DEFAULT;
+ this.userHandle = userHandle;
+ }
+
+ public boolean hasAccess() {
+ if (appOpMode == AppOpsManager.MODE_DEFAULT) {
+ return permissionGranted;
+ }
+ return appOpMode == AppOpsManager.MODE_ALLOWED;
+ }
+ }
+
+ public static final AppFilter FILTER_APP_USAGE = new AppFilter() {
+ @Override
+ public void init() {
+ }
+
+ @Override
+ public boolean filterApp(AppEntry info) {
+ return info.extraInfo != null;
+ }
+ };
+}
diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java
index 1502173..273bcfe 100644
--- a/src/com/android/settings/applications/ManageApplications.java
+++ b/src/com/android/settings/applications/ManageApplications.java
@@ -58,8 +58,10 @@
import com.android.settings.Settings.DomainsURLsAppListActivity;
import com.android.settings.Settings.NotificationAppListActivity;
import com.android.settings.Settings.StorageUseActivity;
+import com.android.settings.Settings.UsageAccessSettingsActivity;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
+import com.android.settings.applications.AppStateUsageBridge.UsageState;
import com.android.settings.applications.ApplicationsState.AppEntry;
import com.android.settings.applications.ApplicationsState.AppFilter;
import com.android.settings.applications.ApplicationsState.CompoundFilter;
@@ -117,6 +119,7 @@
public static final int FILTER_APPS_PERSONAL = 9;
public static final int FILTER_APPS_WORK = 10;
public static final int FILTER_APPS_WITH_DOMAIN_URLS = 11;
+ public static final int FILTER_APPS_USAGE_ACCESS = 12;
// This is the string labels for the filter modes above, the order must be kept in sync.
public static final int[] FILTER_LABELS = new int[] {
@@ -132,6 +135,7 @@
R.string.filter_personal_apps, // Personal
R.string.filter_work_apps, // Work
R.string.filter_with_domain_urls_apps, // Domain URLs
+ R.string.filter_all_apps, // Usage access screen, never displayed
};
// This is the actual mapping to filters from FILTER_ constants above, the order must
// be kept in sync.
@@ -152,6 +156,7 @@
ApplicationsState.FILTER_PERSONAL, // Personal
ApplicationsState.FILTER_WORK, // Work
ApplicationsState.FILTER_WITH_DOMAIN_URLS, // Apps with Domain URLs
+ AppStateUsageBridge.FILTER_APP_USAGE, // Apps with Domain URLs
};
// sort order
@@ -190,6 +195,7 @@
public static final int LIST_TYPE_NOTIFICATION = 1;
public static final int LIST_TYPE_DOMAINS_URLS = 2;
public static final int LIST_TYPE_STORAGE = 3;
+ public static final int LIST_TYPE_USAGE_ACCESS = 4;
private View mRootView;
@@ -230,6 +236,9 @@
mListType = LIST_TYPE_MAIN;
mSortOrder = R.id.sort_order_size;
}
+ } else if (className.equals(UsageAccessSettingsActivity.class.getName())) {
+ mListType = LIST_TYPE_USAGE_ACCESS;
+ getActivity().getActionBar().setTitle(R.string.usage_access_title);
} else {
mListType = LIST_TYPE_MAIN;
}
@@ -300,7 +309,7 @@
contentParent.addView(mSpinnerHeader, 0);
mFilterAdapter.enableFilter(getDefaultFilter());
- if (mListType != LIST_TYPE_STORAGE) {
+ if (mListType == LIST_TYPE_MAIN || mListType == LIST_TYPE_NOTIFICATION) {
if (UserManager.get(getActivity()).getUserProfiles().size() > 1) {
mFilterAdapter.enableFilter(FILTER_APPS_PERSONAL);
mFilterAdapter.enableFilter(FILTER_APPS_WORK);
@@ -331,6 +340,8 @@
return mShowSystem ? FILTER_APPS_ALL : FILTER_APPS_DOWNLOADED_AND_LAUNCHER;
case LIST_TYPE_DOMAINS_URLS:
return FILTER_APPS_WITH_DOMAIN_URLS;
+ case LIST_TYPE_USAGE_ACCESS:
+ return FILTER_APPS_USAGE_ACCESS;
default:
return FILTER_APPS_ALL;
}
@@ -347,6 +358,8 @@
return MetricsLogger.MANAGE_DOMAIN_URLS;
case LIST_TYPE_STORAGE:
return InstrumentedFragment.VIEW_CATEGORY_STORAGE_APPS;
+ case LIST_TYPE_USAGE_ACCESS:
+ return MetricsLogger.USAGE_ACCESS;
default:
return MetricsLogger.VIEW_UNKNOWN;
}
@@ -398,7 +411,7 @@
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == INSTALLED_APP_DETAILS && mCurrentPkgName != null) {
if (mListType == LIST_TYPE_NOTIFICATION) {
- mApplications.mNotifBridge.forceUpdate(mCurrentPkgName, mCurrentUid);
+ mApplications.mExtraInfoBridge.forceUpdate(mCurrentPkgName, mCurrentUid);
} else {
mApplicationsState.requestSize(mCurrentPkgName, UserHandle.getUserId(mCurrentUid));
}
@@ -408,30 +421,39 @@
// utility method used to start sub activity
private void startApplicationDetailsActivity() {
Activity activity = getActivity();
- if (mListType == LIST_TYPE_NOTIFICATION) {
- activity.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
- .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- .putExtra(Settings.EXTRA_APP_PACKAGE, mCurrentPkgName)
- .putExtra(Settings.EXTRA_APP_UID, mCurrentUid));
- } else if (mListType == LIST_TYPE_DOMAINS_URLS) {
- final String title = getString(R.string.auto_launch_label);
- startAppInfoFragment(AppLaunchSettings.class, title);
- } else {
+ switch (mListType) {
+ case LIST_TYPE_NOTIFICATION:
+ activity.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ .putExtra(Settings.EXTRA_APP_PACKAGE, mCurrentPkgName)
+ .putExtra(Settings.EXTRA_APP_UID, mCurrentUid));
+ break;
+ case LIST_TYPE_DOMAINS_URLS:
+ startAppInfoFragment(AppLaunchSettings.class, R.string.auto_launch_label);
+ break;
+ case LIST_TYPE_USAGE_ACCESS:
+ startAppInfoFragment(UsageAccessDetails.class, R.string.usage_access);
+ break;
+ case LIST_TYPE_STORAGE:
+ startAppInfoFragment(AppStorageSettings.class, R.string.storage_settings);
+ 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.
- Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
- intent.setData(Uri.fromParts("package", mCurrentPkgName, null));
- activity.startActivityAsUser(intent, new UserHandle(UserHandle.getUserId(mCurrentUid)));
+ default:
+ startAppInfoFragment(InstalledAppDetails.class, R.string.application_info_label);
+ break;
}
}
- private void startAppInfoFragment(Class<? extends AppInfoBase> fragment, CharSequence title) {
+ private void startAppInfoFragment(Class<? extends AppInfoBase> fragment, int titleRes) {
Bundle args = new Bundle();
- args.putString("package", mCurrentPkgName);
+ args.putString(AppInfoBase.ARG_PACKAGE_NAME, mCurrentPkgName);
- SettingsActivity sa = (SettingsActivity) getActivity();
- sa.startPreferencePanel(fragment.getName(), args, -1, title, this, 0);
+ Intent intent = Utils.onBuildStartFragmentIntent(getActivity(), fragment.getName(), args,
+ null, titleRes, null, false);
+ getActivity().startActivityForResultAsUser(intent, INSTALLED_APP_DETAILS,
+ new UserHandle(UserHandle.getUserId(mCurrentUid)));
}
@Override
@@ -653,14 +675,14 @@
* The order of applications in the list is mirrored in mAppLocalList
*/
static class ApplicationsAdapter extends BaseAdapter implements Filterable,
- ApplicationsState.Callbacks, AppStateNotificationBridge.Callback,
+ ApplicationsState.Callbacks, AppStateBaseBridge.Callback,
AbsListView.RecyclerListener {
private final ApplicationsState mState;
private final ApplicationsState.Session mSession;
private final ManageApplications mManageApplications;
private final Context mContext;
private final ArrayList<View> mActive = new ArrayList<View>();
- private final AppStateNotificationBridge mNotifBridge;
+ private final AppStateBaseBridge mExtraInfoBridge;
private int mFilterMode;
private ArrayList<ApplicationsState.AppEntry> mBaseEntries;
private ArrayList<ApplicationsState.AppEntry> mEntries;
@@ -700,11 +722,12 @@
mPm = mContext.getPackageManager();
mFilterMode = filterMode;
if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) {
- mNotifBridge = new AppStateNotificationBridge(
- mContext.getPackageManager(), mState,
- manageApplications.mNotifBackend, this);
+ mExtraInfoBridge = new AppStateNotificationBridge(mContext.getPackageManager(),
+ mState, this, manageApplications.mNotifBackend);
+ } else if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) {
+ mExtraInfoBridge = new AppStateUsageBridge(mContext, mState, this);
} else {
- mNotifBridge = null;
+ mExtraInfoBridge = null;
}
}
@@ -724,8 +747,8 @@
mResumed = true;
mSession.resume();
mLastSortMode = sort;
- if (mNotifBridge != null) {
- mNotifBridge.resume();
+ if (mExtraInfoBridge != null) {
+ mExtraInfoBridge.resume();
}
rebuild(true);
} else {
@@ -737,16 +760,16 @@
if (mResumed) {
mResumed = false;
mSession.pause();
- if (mNotifBridge != null) {
- mNotifBridge.pause();
+ if (mExtraInfoBridge != null) {
+ mExtraInfoBridge.pause();
}
}
}
public void release() {
mSession.release();
- if (mNotifBridge != null) {
- mNotifBridge.release();
+ if (mExtraInfoBridge != null) {
+ mExtraInfoBridge.release();
}
}
@@ -809,6 +832,10 @@
Utils.handleLoadingContainer(mManageApplications.mLoadingContainer,
mManageApplications.mListContainer, true, true);
}
+ if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) {
+ // No enabled or disabled filters for usage access.
+ return;
+ }
mManageApplications.setHasDisabled(hasDisabledApps());
}
@@ -849,10 +876,8 @@
}
@Override
- public void onNotificationInfoUpdated() {
- if (mFilterMode != mManageApplications.getDefaultFilter()) {
- rebuild(false);
- }
+ public void onExtraInfoUpdated() {
+ rebuild(false);
}
@Override
@@ -897,9 +922,7 @@
AppViewHolder holder = (AppViewHolder)mActive.get(i).getTag();
if (holder.entry.info.packageName.equals(packageName)) {
synchronized (holder.entry) {
- if (mManageApplications.mListType != LIST_TYPE_NOTIFICATION) {
- holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize);
- }
+ updateSummary(holder);
}
if (holder.entry.info.packageName.equals(mManageApplications.mCurrentPkgName)
&& mLastSortMode == R.id.sort_order_size) {
@@ -967,24 +990,7 @@
if (entry.icon != null) {
holder.appIcon.setImageDrawable(entry.icon);
}
- switch (mManageApplications.mListType) {
- case LIST_TYPE_NOTIFICATION:
- if (entry.extraInfo != null) {
- holder.summary.setText(InstalledAppDetails.getNotificationSummary(
- (AppRow) entry.extraInfo, mContext));
- } else {
- holder.summary.setText("");
- }
- break;
-
- case LIST_TYPE_DOMAINS_URLS:
- holder.summary.setText(getDomainsSummary(entry.info.packageName));
- break;
-
- default:
- holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize);
- break;
- }
+ updateSummary(holder);
convertView.setEnabled(isAppEntryViewEnabled(entry));
if ((entry.info.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
holder.disabled.setVisibility(View.VISIBLE);
@@ -1002,6 +1008,36 @@
return convertView;
}
+ private void updateSummary(AppViewHolder holder) {
+ switch (mManageApplications.mListType) {
+ case LIST_TYPE_NOTIFICATION:
+ if (holder.entry.extraInfo != null) {
+ holder.summary.setText(InstalledAppDetails.getNotificationSummary(
+ (AppRow) holder.entry.extraInfo, mContext));
+ } else {
+ holder.summary.setText(null);
+ }
+ break;
+
+ case LIST_TYPE_DOMAINS_URLS:
+ holder.summary.setText(getDomainsSummary(holder.entry.info.packageName));
+ break;
+
+ case LIST_TYPE_USAGE_ACCESS:
+ if (holder.entry.extraInfo != null) {
+ holder.summary.setText(((UsageState) holder.entry.extraInfo).hasAccess() ?
+ R.string.switch_on_text : R.string.switch_off_text);
+ } else {
+ holder.summary.setText(null);
+ }
+ break;
+
+ default:
+ holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize);
+ break;
+ }
+ }
+
@Override
public Filter getFilter() {
return mFilter;
diff --git a/src/com/android/settings/applications/UsageAccessDetails.java b/src/com/android/settings/applications/UsageAccessDetails.java
new file mode 100644
index 0000000..a11d7a7
--- /dev/null
+++ b/src/com/android/settings/applications/UsageAccessDetails.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2015 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 android.app.AlertDialog;
+import android.app.AppOpsManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.SwitchPreference;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settings.InstrumentedFragment;
+import com.android.settings.R;
+import com.android.settings.applications.AppStateUsageBridge.UsageState;
+
+public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenceChangeListener,
+ OnPreferenceClickListener {
+
+ private static final String KEY_USAGE_SWITCH = "usage_switch";
+ private static final String KEY_USAGE_PREFS = "app_usage_preference";
+
+ // Use a bridge to get the usage stats but don't initialize it to connect with all state.
+ // TODO: Break out this functionality into its own class.
+ private AppStateUsageBridge mUsageBridge;
+ private AppOpsManager mAppOpsManager;
+ private SwitchPreference mSwitchPref;
+ private Preference mUsagePrefs;
+ private Intent mSettingsIntent;
+ private UsageState mUsageState;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Context context = getActivity();
+ mUsageBridge = new AppStateUsageBridge(context, mState, null);
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+
+ addPreferencesFromResource(R.xml.usage_access_details);
+ mSwitchPref = (SwitchPreference) findPreference(KEY_USAGE_SWITCH);
+ mUsagePrefs = findPreference(KEY_USAGE_PREFS);
+
+ mSwitchPref.setOnPreferenceChangeListener(this);
+ mUsagePrefs.setOnPreferenceClickListener(this);
+
+ mSettingsIntent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Settings.INTENT_CATEGORY_USAGE_ACCESS_CONFIG)
+ .setPackage(mPackageName);
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference == mUsagePrefs) {
+ if (mSettingsIntent != null) {
+ try {
+ getActivity().startActivityAsUser(mSettingsIntent, new UserHandle(mUserId));
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "Unable to launch app usage access settings " + mSettingsIntent, e);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference == mSwitchPref) {
+ if (mUsageState != null && (Boolean) newValue != mUsageState.hasAccess()) {
+ setHasAccess(!mUsageState.hasAccess());
+ refreshUi();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void setHasAccess(boolean newState) {
+ mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS, mPackageInfo.applicationInfo.uid,
+ mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+ }
+
+ @Override
+ protected boolean refreshUi() {
+ mUsageState = mUsageBridge.getUsageInfo(mPackageName,
+ mPackageInfo.applicationInfo.uid);
+
+ boolean hasAccess = mUsageState.hasAccess();
+ mSwitchPref.setChecked(hasAccess);
+ mUsagePrefs.setEnabled(hasAccess);
+
+ ResolveInfo resolveInfo = mPm.resolveActivityAsUser(mSettingsIntent,
+ PackageManager.GET_META_DATA, mUserId);
+ if (resolveInfo != null) {
+ if (findPreference(KEY_USAGE_PREFS) == null) {
+ getPreferenceScreen().addPreference(mUsagePrefs);
+ }
+ Bundle metaData = resolveInfo.activityInfo.metaData;
+ mSettingsIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName,
+ resolveInfo.activityInfo.name));
+ if (metaData != null
+ && metaData.containsKey(Settings.METADATA_USAGE_ACCESS_REASON)) {
+ mSwitchPref.setSummary(
+ metaData.getString(Settings.METADATA_USAGE_ACCESS_REASON));
+ }
+ } else {
+ if (findPreference(KEY_USAGE_PREFS) != null) {
+ getPreferenceScreen().removePreference(mUsagePrefs);
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ protected AlertDialog createDialog(int id, int errorCode) {
+ return null;
+ }
+
+ @Override
+ protected int getMetricsCategory() {
+ return InstrumentedFragment.VIEW_CATEGORY_USAGE_ACCESS_DETAIL;
+ }
+
+}