From d8da51ccfecf16f2c06f788e6bcbc232d1f0cb32 Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Fri, 17 Apr 2015 14:34:12 -0400 Subject: [PATCH] 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 --- AndroidManifest.xml | 2 +- res/values/strings.xml | 16 +- res/xml/security_settings_misc.xml | 6 +- res/xml/usage_access_details.xml | 33 + .../settings/InstrumentedFragment.java | 1 + .../android/settings/SettingsActivity.java | 3 +- .../android/settings/UsageAccessSettings.java | 569 ------------------ .../settings/applications/AppInfoBase.java | 1 - .../applications/AppStateBaseBridge.java | 158 +++++ .../AppStateNotificationBridge.java | 137 +---- .../applications/AppStateUsageBridge.java | 265 ++++++++ .../applications/ManageApplications.java | 146 +++-- .../applications/UsageAccessDetails.java | 148 +++++ 13 files changed, 728 insertions(+), 757 deletions(-) create mode 100644 res/xml/usage_access_details.xml delete mode 100644 src/com/android/settings/UsageAccessSettings.java create mode 100644 src/com/android/settings/applications/AppStateBaseBridge.java create mode 100644 src/com/android/settings/applications/AppStateUsageBridge.java create mode 100644 src/com/android/settings/applications/UsageAccessDetails.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f47ef0d95c7..aec34bcf8ab 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1228,7 +1228,7 @@ + android:value="com.android.settings.applications.ManageApplications" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index 72adf4e2edb..080cd3e3d6a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4497,10 +4497,6 @@ Apps with usage access - - Allow access? - - If you allow access, this app can view general information about the apps on your device, such as how often you use them. Emergency tone @@ -6461,4 +6457,16 @@ Apps storage + + Usage access + + + Permit usage access + + + App usage preferences + + + 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. + diff --git a/res/xml/security_settings_misc.xml b/res/xml/security_settings_misc.xml index 71f9ffac328..b67e1b7c394 100644 --- a/res/xml/security_settings_misc.xml +++ b/res/xml/security_settings_misc.xml @@ -117,7 +117,11 @@ + android:fragment="com.android.settings.applications.ManageApplications"> + + diff --git a/res/xml/usage_access_details.xml b/res/xml/usage_access_details.xml new file mode 100644 index 00000000000..d8b3bb1af4a --- /dev/null +++ b/res/xml/usage_access_details.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/src/com/android/settings/InstrumentedFragment.java b/src/com/android/settings/InstrumentedFragment.java index 883c67ccac9..d239d4e1ca0 100644 --- a/src/com/android/settings/InstrumentedFragment.java +++ b/src/com/android/settings/InstrumentedFragment.java @@ -28,6 +28,7 @@ public abstract class InstrumentedFragment extends PreferenceFragment { 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 e8be34a4368..caf2491612b 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -76,6 +76,7 @@ import com.android.settings.accounts.AccountSyncSettings; 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 @@ public class SettingsActivity extends Activity 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 704c3c51bdd..00000000000 --- 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 { - 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>> { - - private final Context mContext; - private final PackageManager mPackageManager; - private final IPackageManager mIPackageManager; - private final UserManager mUserManager; - private final List 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> doInBackground(Void... params) { - final String[] packages; - SparseArray> 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 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 entriesForProfile = entries.get(profileId); - if (entriesForProfile == null) { - continue; - } - final List 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 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 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 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> 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 oldEntriesForProfile = mPackageEntryMap - .valueAt(profileIndex); - final int oldPackageCount = oldEntriesForProfile.size(); - - final ArrayMap 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 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 entriesForProfile = - mPackageEntryMap.get(mProfiles.get(i).getIdentifier()); - if (entriesForProfile != null) { - List 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> 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 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 a7dc5008237..56fe885461e 100644 --- a/src/com/android/settings/applications/AppInfoBase.java +++ b/src/com/android/settings/applications/AppInfoBase.java @@ -32,7 +32,6 @@ import android.os.IBinder; 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 00000000000..04eb77bcf02 --- /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 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 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 0434c0e80ca..1aa7ebf123a 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 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 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 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 00000000000..22f19b23f17 --- /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 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 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> entries = getEntries(); + + // Load state info. + loadPermissionsStates(entries); + loadAppOpsStates(entries); + + // Map states to application info. + List 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 userMap = entries.get(userId); + app.extraInfo = userMap != null ? userMap.get(app.info.packageName) : null; + } + } + + private SparseArray> 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> entries = new SparseArray<>(); + for (final UserHandle profile : mProfiles) { + final ArrayMap 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> 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 entriesForProfile = entries.get(profileId); + if (entriesForProfile == null) { + continue; + } + @SuppressWarnings("unchecked") + final List 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> entries) { + // Find out which packages have been granted permission from AppOps. + final List 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 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 1502173509f..273bcfe52cd 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.AllApplicationsActivity; 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 class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment // 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 fragment, CharSequence title) { + private void startAppInfoFragment(Class 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 @@ public class ManageApplications extends InstrumentedFragment * 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 mActive = new ArrayList(); - private final AppStateNotificationBridge mNotifBridge; + private final AppStateBaseBridge mExtraInfoBridge; private int mFilterMode; private ArrayList mBaseEntries; private ArrayList mEntries; @@ -700,11 +722,12 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment mResumed = true; mSession.resume(); mLastSortMode = sort; - if (mNotifBridge != null) { - mNotifBridge.resume(); + if (mExtraInfoBridge != null) { + mExtraInfoBridge.resume(); } rebuild(true); } else { @@ -737,16 +760,16 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment } @Override - public void onNotificationInfoUpdated() { - if (mFilterMode != mManageApplications.getDefaultFilter()) { - rebuild(false); - } + public void onExtraInfoUpdated() { + rebuild(false); } @Override @@ -897,9 +922,7 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 @@ public class ManageApplications extends InstrumentedFragment 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 00000000000..a11d7a7b14b --- /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; + } + +}