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
This commit is contained in:
Jason Monk
2015-04-17 14:34:12 -04:00
parent 6d61061518
commit d8da51ccfe
13 changed files with 728 additions and 757 deletions

View File

@@ -1228,7 +1228,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS" <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" <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/security_settings" /> android:resource="@id/security_settings" />
</activity> </activity>

View File

@@ -4497,10 +4497,6 @@
<!-- Title of Usage Access preference item [CHAR LIMIT=30] --> <!-- Title of Usage Access preference item [CHAR LIMIT=30] -->
<string name="usage_access_title">Apps with usage access</string> <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 --> <!-- Sound settings screen, setting check box label -->
<string name="emergency_tone_title">Emergency tone</string> <string name="emergency_tone_title">Emergency tone</string>
@@ -6461,4 +6457,16 @@
<!-- Title of app storage screen [CHAR LIMIT=30] --> <!-- Title of app storage screen [CHAR LIMIT=30] -->
<string name="apps_storage">Apps storage</string> <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> </resources>

View File

@@ -117,7 +117,11 @@
<Preference android:key="usage_access" <Preference android:key="usage_access"
android:title="@string/usage_access_title" 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> </PreferenceCategory>

View File

@@ -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>

View File

@@ -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_DEFAULT_APPS = VIEW_CATEGORY_UNDECLARED + 1;
public static final int VIEW_CATEGORY_STORAGE_APPS = VIEW_CATEGORY_UNDECLARED + 2; 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. * Declare the view of this category.

View File

@@ -76,6 +76,7 @@ import com.android.settings.accounts.AccountSyncSettings;
import com.android.settings.applications.InstalledAppDetails; import com.android.settings.applications.InstalledAppDetails;
import com.android.settings.applications.ManageApplications; import com.android.settings.applications.ManageApplications;
import com.android.settings.applications.ProcessStatsUi; import com.android.settings.applications.ProcessStatsUi;
import com.android.settings.applications.UsageAccessDetails;
import com.android.settings.bluetooth.BluetoothSettings; import com.android.settings.bluetooth.BluetoothSettings;
import com.android.settings.dashboard.DashboardCategory; import com.android.settings.dashboard.DashboardCategory;
import com.android.settings.dashboard.DashboardSummary; import com.android.settings.dashboard.DashboardSummary;
@@ -300,7 +301,7 @@ public class SettingsActivity extends Activity
NotificationStation.class.getName(), NotificationStation.class.getName(),
LocationSettings.class.getName(), LocationSettings.class.getName(),
SecuritySettings.class.getName(), SecuritySettings.class.getName(),
UsageAccessSettings.class.getName(), UsageAccessDetails.class.getName(),
PrivacySettings.class.getName(), PrivacySettings.class.getName(),
DeviceAdminSettings.class.getName(), DeviceAdminSettings.class.getName(),
AccessibilitySettings.class.getName(), AccessibilitySettings.class.getName(),

View File

@@ -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();
}
}
}
}

View File

@@ -32,7 +32,6 @@ import android.os.IBinder;
import android.os.ServiceManager; import android.os.ServiceManager;
import android.os.UserHandle; import android.os.UserHandle;
import android.os.UserManager; import android.os.UserManager;
import android.preference.PreferenceFragment;
import android.util.Log; import android.util.Log;
import com.android.settings.InstrumentedPreferenceFragment; import com.android.settings.InstrumentedPreferenceFragment;

View File

@@ -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();
}
}

View File

@@ -16,156 +16,43 @@
package com.android.settings.applications; package com.android.settings.applications;
import android.content.pm.PackageManager; 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.AppEntry;
import com.android.settings.applications.ApplicationsState.AppFilter; 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;
import com.android.settings.notification.NotificationBackend.AppRow; import com.android.settings.notification.NotificationBackend.AppRow;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** /**
* Connects the info provided by ApplicationsState and the NotificationBackend. * Connects the info provided by ApplicationsState and the NotificationBackend.
* Also provides app filters that can use the notification data. * 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 NotificationBackend mNotifBackend;
private final Session mAppSession;
private final Callback mCallback;
private final BackgroundHandler mHandler;
private final MainHandler mMainHandler;
private final PackageManager mPm; private final PackageManager mPm;
public AppStateNotificationBridge(PackageManager pm, ApplicationsState appState, public AppStateNotificationBridge(PackageManager pm, ApplicationsState appState,
NotificationBackend notifBackend, Callback callback) { Callback callback, NotificationBackend notifBackend) {
mAppState = appState; super(appState, callback);
mPm = pm; mPm = pm;
mAppSession = mAppState.newSession(this);
mNotifBackend = notifBackend; 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 @Override
public void onPackageListChanged() { protected void loadAllExtraInfo() {
mHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL); ArrayList<AppEntry> apps = mAppSession.getAllApps();
} final int N = apps.size();
for (int i = 0; i < N; i++) {
@Override AppEntry app = apps.get(i);
public void onLoadEntriesCompleted() { app.extraInfo = mNotifBackend.loadAppRow(mPm, app.info);
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;
}
} }
} }
private class BackgroundHandler extends Handler { @Override
private static final int MSG_LOAD_ALL = 1; protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
private static final int MSG_FORCE_LOAD_PKG = 2; app.extraInfo = mNotifBackend.loadAppRow(mPm, app.info);
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();
} }
public static final AppFilter FILTER_APP_NOTIFICATION_BLOCKED = new AppFilter() { public static final AppFilter FILTER_APP_NOTIFICATION_BLOCKED = new AppFilter() {

View File

@@ -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;
}
};
}

View File

@@ -58,8 +58,10 @@ import com.android.settings.Settings.AllApplicationsActivity;
import com.android.settings.Settings.DomainsURLsAppListActivity; import com.android.settings.Settings.DomainsURLsAppListActivity;
import com.android.settings.Settings.NotificationAppListActivity; import com.android.settings.Settings.NotificationAppListActivity;
import com.android.settings.Settings.StorageUseActivity; import com.android.settings.Settings.StorageUseActivity;
import com.android.settings.Settings.UsageAccessSettingsActivity;
import com.android.settings.SettingsActivity; import com.android.settings.SettingsActivity;
import com.android.settings.Utils; 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.AppEntry;
import com.android.settings.applications.ApplicationsState.AppFilter; import com.android.settings.applications.ApplicationsState.AppFilter;
import com.android.settings.applications.ApplicationsState.CompoundFilter; 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_PERSONAL = 9;
public static final int FILTER_APPS_WORK = 10; public static final int FILTER_APPS_WORK = 10;
public static final int FILTER_APPS_WITH_DOMAIN_URLS = 11; 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. // 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[] { 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_personal_apps, // Personal
R.string.filter_work_apps, // Work R.string.filter_work_apps, // Work
R.string.filter_with_domain_urls_apps, // Domain URLs 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 // This is the actual mapping to filters from FILTER_ constants above, the order must
// be kept in sync. // be kept in sync.
@@ -152,6 +156,7 @@ public class ManageApplications extends InstrumentedFragment
ApplicationsState.FILTER_PERSONAL, // Personal ApplicationsState.FILTER_PERSONAL, // Personal
ApplicationsState.FILTER_WORK, // Work ApplicationsState.FILTER_WORK, // Work
ApplicationsState.FILTER_WITH_DOMAIN_URLS, // Apps with Domain URLs ApplicationsState.FILTER_WITH_DOMAIN_URLS, // Apps with Domain URLs
AppStateUsageBridge.FILTER_APP_USAGE, // Apps with Domain URLs
}; };
// sort order // 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_NOTIFICATION = 1;
public static final int LIST_TYPE_DOMAINS_URLS = 2; public static final int LIST_TYPE_DOMAINS_URLS = 2;
public static final int LIST_TYPE_STORAGE = 3; public static final int LIST_TYPE_STORAGE = 3;
public static final int LIST_TYPE_USAGE_ACCESS = 4;
private View mRootView; private View mRootView;
@@ -230,6 +236,9 @@ public class ManageApplications extends InstrumentedFragment
mListType = LIST_TYPE_MAIN; mListType = LIST_TYPE_MAIN;
mSortOrder = R.id.sort_order_size; 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 { } else {
mListType = LIST_TYPE_MAIN; mListType = LIST_TYPE_MAIN;
} }
@@ -300,7 +309,7 @@ public class ManageApplications extends InstrumentedFragment
contentParent.addView(mSpinnerHeader, 0); contentParent.addView(mSpinnerHeader, 0);
mFilterAdapter.enableFilter(getDefaultFilter()); mFilterAdapter.enableFilter(getDefaultFilter());
if (mListType != LIST_TYPE_STORAGE) { if (mListType == LIST_TYPE_MAIN || mListType == LIST_TYPE_NOTIFICATION) {
if (UserManager.get(getActivity()).getUserProfiles().size() > 1) { if (UserManager.get(getActivity()).getUserProfiles().size() > 1) {
mFilterAdapter.enableFilter(FILTER_APPS_PERSONAL); mFilterAdapter.enableFilter(FILTER_APPS_PERSONAL);
mFilterAdapter.enableFilter(FILTER_APPS_WORK); mFilterAdapter.enableFilter(FILTER_APPS_WORK);
@@ -331,6 +340,8 @@ public class ManageApplications extends InstrumentedFragment
return mShowSystem ? FILTER_APPS_ALL : FILTER_APPS_DOWNLOADED_AND_LAUNCHER; return mShowSystem ? FILTER_APPS_ALL : FILTER_APPS_DOWNLOADED_AND_LAUNCHER;
case LIST_TYPE_DOMAINS_URLS: case LIST_TYPE_DOMAINS_URLS:
return FILTER_APPS_WITH_DOMAIN_URLS; return FILTER_APPS_WITH_DOMAIN_URLS;
case LIST_TYPE_USAGE_ACCESS:
return FILTER_APPS_USAGE_ACCESS;
default: default:
return FILTER_APPS_ALL; return FILTER_APPS_ALL;
} }
@@ -347,6 +358,8 @@ public class ManageApplications extends InstrumentedFragment
return MetricsLogger.MANAGE_DOMAIN_URLS; return MetricsLogger.MANAGE_DOMAIN_URLS;
case LIST_TYPE_STORAGE: case LIST_TYPE_STORAGE:
return InstrumentedFragment.VIEW_CATEGORY_STORAGE_APPS; return InstrumentedFragment.VIEW_CATEGORY_STORAGE_APPS;
case LIST_TYPE_USAGE_ACCESS:
return MetricsLogger.USAGE_ACCESS;
default: default:
return MetricsLogger.VIEW_UNKNOWN; return MetricsLogger.VIEW_UNKNOWN;
} }
@@ -398,7 +411,7 @@ public class ManageApplications extends InstrumentedFragment
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == INSTALLED_APP_DETAILS && mCurrentPkgName != null) { if (requestCode == INSTALLED_APP_DETAILS && mCurrentPkgName != null) {
if (mListType == LIST_TYPE_NOTIFICATION) { if (mListType == LIST_TYPE_NOTIFICATION) {
mApplications.mNotifBridge.forceUpdate(mCurrentPkgName, mCurrentUid); mApplications.mExtraInfoBridge.forceUpdate(mCurrentPkgName, mCurrentUid);
} else { } else {
mApplicationsState.requestSize(mCurrentPkgName, UserHandle.getUserId(mCurrentUid)); mApplicationsState.requestSize(mCurrentPkgName, UserHandle.getUserId(mCurrentUid));
} }
@@ -408,30 +421,39 @@ public class ManageApplications extends InstrumentedFragment
// utility method used to start sub activity // utility method used to start sub activity
private void startApplicationDetailsActivity() { private void startApplicationDetailsActivity() {
Activity activity = getActivity(); Activity activity = getActivity();
if (mListType == LIST_TYPE_NOTIFICATION) { switch (mListType) {
activity.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) case LIST_TYPE_NOTIFICATION:
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, mCurrentPkgName) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(Settings.EXTRA_APP_UID, mCurrentUid)); .putExtra(Settings.EXTRA_APP_PACKAGE, mCurrentPkgName)
} else if (mListType == LIST_TYPE_DOMAINS_URLS) { .putExtra(Settings.EXTRA_APP_UID, mCurrentUid));
final String title = getString(R.string.auto_launch_label); break;
startAppInfoFragment(AppLaunchSettings.class, title); case LIST_TYPE_DOMAINS_URLS:
} else { 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 // 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. // 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. // Maybe when they load the list of apps that contains managed profile apps.
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); default:
intent.setData(Uri.fromParts("package", mCurrentPkgName, null)); startAppInfoFragment(InstalledAppDetails.class, R.string.application_info_label);
activity.startActivityAsUser(intent, new UserHandle(UserHandle.getUserId(mCurrentUid))); break;
} }
} }
private void startAppInfoFragment(Class<? extends AppInfoBase> fragment, CharSequence title) { private void startAppInfoFragment(Class<? extends AppInfoBase> fragment, int titleRes) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString("package", mCurrentPkgName); args.putString(AppInfoBase.ARG_PACKAGE_NAME, mCurrentPkgName);
SettingsActivity sa = (SettingsActivity) getActivity(); Intent intent = Utils.onBuildStartFragmentIntent(getActivity(), fragment.getName(), args,
sa.startPreferencePanel(fragment.getName(), args, -1, title, this, 0); null, titleRes, null, false);
getActivity().startActivityForResultAsUser(intent, INSTALLED_APP_DETAILS,
new UserHandle(UserHandle.getUserId(mCurrentUid)));
} }
@Override @Override
@@ -653,14 +675,14 @@ public class ManageApplications extends InstrumentedFragment
* The order of applications in the list is mirrored in mAppLocalList * The order of applications in the list is mirrored in mAppLocalList
*/ */
static class ApplicationsAdapter extends BaseAdapter implements Filterable, static class ApplicationsAdapter extends BaseAdapter implements Filterable,
ApplicationsState.Callbacks, AppStateNotificationBridge.Callback, ApplicationsState.Callbacks, AppStateBaseBridge.Callback,
AbsListView.RecyclerListener { AbsListView.RecyclerListener {
private final ApplicationsState mState; private final ApplicationsState mState;
private final ApplicationsState.Session mSession; private final ApplicationsState.Session mSession;
private final ManageApplications mManageApplications; private final ManageApplications mManageApplications;
private final Context mContext; private final Context mContext;
private final ArrayList<View> mActive = new ArrayList<View>(); private final ArrayList<View> mActive = new ArrayList<View>();
private final AppStateNotificationBridge mNotifBridge; private final AppStateBaseBridge mExtraInfoBridge;
private int mFilterMode; private int mFilterMode;
private ArrayList<ApplicationsState.AppEntry> mBaseEntries; private ArrayList<ApplicationsState.AppEntry> mBaseEntries;
private ArrayList<ApplicationsState.AppEntry> mEntries; private ArrayList<ApplicationsState.AppEntry> mEntries;
@@ -700,11 +722,12 @@ public class ManageApplications extends InstrumentedFragment
mPm = mContext.getPackageManager(); mPm = mContext.getPackageManager();
mFilterMode = filterMode; mFilterMode = filterMode;
if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) {
mNotifBridge = new AppStateNotificationBridge( mExtraInfoBridge = new AppStateNotificationBridge(mContext.getPackageManager(),
mContext.getPackageManager(), mState, mState, this, manageApplications.mNotifBackend);
manageApplications.mNotifBackend, this); } else if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) {
mExtraInfoBridge = new AppStateUsageBridge(mContext, mState, this);
} else { } else {
mNotifBridge = null; mExtraInfoBridge = null;
} }
} }
@@ -724,8 +747,8 @@ public class ManageApplications extends InstrumentedFragment
mResumed = true; mResumed = true;
mSession.resume(); mSession.resume();
mLastSortMode = sort; mLastSortMode = sort;
if (mNotifBridge != null) { if (mExtraInfoBridge != null) {
mNotifBridge.resume(); mExtraInfoBridge.resume();
} }
rebuild(true); rebuild(true);
} else { } else {
@@ -737,16 +760,16 @@ public class ManageApplications extends InstrumentedFragment
if (mResumed) { if (mResumed) {
mResumed = false; mResumed = false;
mSession.pause(); mSession.pause();
if (mNotifBridge != null) { if (mExtraInfoBridge != null) {
mNotifBridge.pause(); mExtraInfoBridge.pause();
} }
} }
} }
public void release() { public void release() {
mSession.release(); mSession.release();
if (mNotifBridge != null) { if (mExtraInfoBridge != null) {
mNotifBridge.release(); mExtraInfoBridge.release();
} }
} }
@@ -809,6 +832,10 @@ public class ManageApplications extends InstrumentedFragment
Utils.handleLoadingContainer(mManageApplications.mLoadingContainer, Utils.handleLoadingContainer(mManageApplications.mLoadingContainer,
mManageApplications.mListContainer, true, true); mManageApplications.mListContainer, true, true);
} }
if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) {
// No enabled or disabled filters for usage access.
return;
}
mManageApplications.setHasDisabled(hasDisabledApps()); mManageApplications.setHasDisabled(hasDisabledApps());
} }
@@ -849,10 +876,8 @@ public class ManageApplications extends InstrumentedFragment
} }
@Override @Override
public void onNotificationInfoUpdated() { public void onExtraInfoUpdated() {
if (mFilterMode != mManageApplications.getDefaultFilter()) { rebuild(false);
rebuild(false);
}
} }
@Override @Override
@@ -897,9 +922,7 @@ public class ManageApplications extends InstrumentedFragment
AppViewHolder holder = (AppViewHolder)mActive.get(i).getTag(); AppViewHolder holder = (AppViewHolder)mActive.get(i).getTag();
if (holder.entry.info.packageName.equals(packageName)) { if (holder.entry.info.packageName.equals(packageName)) {
synchronized (holder.entry) { synchronized (holder.entry) {
if (mManageApplications.mListType != LIST_TYPE_NOTIFICATION) { updateSummary(holder);
holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize);
}
} }
if (holder.entry.info.packageName.equals(mManageApplications.mCurrentPkgName) if (holder.entry.info.packageName.equals(mManageApplications.mCurrentPkgName)
&& mLastSortMode == R.id.sort_order_size) { && mLastSortMode == R.id.sort_order_size) {
@@ -967,24 +990,7 @@ public class ManageApplications extends InstrumentedFragment
if (entry.icon != null) { if (entry.icon != null) {
holder.appIcon.setImageDrawable(entry.icon); holder.appIcon.setImageDrawable(entry.icon);
} }
switch (mManageApplications.mListType) { updateSummary(holder);
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;
}
convertView.setEnabled(isAppEntryViewEnabled(entry)); convertView.setEnabled(isAppEntryViewEnabled(entry));
if ((entry.info.flags&ApplicationInfo.FLAG_INSTALLED) == 0) { if ((entry.info.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
holder.disabled.setVisibility(View.VISIBLE); holder.disabled.setVisibility(View.VISIBLE);
@@ -1002,6 +1008,36 @@ public class ManageApplications extends InstrumentedFragment
return convertView; 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 @Override
public Filter getFilter() { public Filter getFilter() {
return mFilter; return mFilter;

View File

@@ -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;
}
}