blob: 8e8e533117a38cd2758a8d22a898795f02642b53 [file] [log] [blame]
Adam Lesinski468d3912014-07-22 10:01:08 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.settings;
18
19import com.android.internal.content.PackageMonitor;
20
21import android.Manifest;
22import android.app.ActivityThread;
23import android.app.AppOpsManager;
24import android.content.Context;
25import android.content.pm.IPackageManager;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.Looper;
31import android.os.RemoteException;
32import android.preference.Preference;
33import android.preference.SwitchPreference;
34import android.util.ArrayMap;
35import android.util.Log;
36
37import java.util.List;
38
39public class UsageAccessSettings extends SettingsPreferenceFragment implements
40 Preference.OnPreferenceChangeListener {
41
42 private static final String TAG = "UsageAccessSettings";
43
44 private static final String[] PM_USAGE_STATS_PERMISSION = new String[] {
45 Manifest.permission.PACKAGE_USAGE_STATS
46 };
47
48 private static final int[] APP_OPS_OP_CODES = new int[] {
49 AppOpsManager.OP_GET_USAGE_STATS
50 };
51
52 private static class PackageEntry {
53 public PackageEntry(String packageName) {
54 this.packageName = packageName;
55 this.appOpMode = AppOpsManager.MODE_DEFAULT;
56 }
57
58 final String packageName;
59 PackageInfo packageInfo;
60 boolean permissionGranted;
61 int appOpMode;
62
63 SwitchPreference preference;
64 }
65
66 /**
67 * Fetches the list of Apps that are requesting access to the UsageStats API and updates
68 * the PreferenceScreen with the results when complete.
69 */
70 private class AppsRequestingAccessFetcher extends
71 AsyncTask<Void, Void, ArrayMap<String, PackageEntry>> {
72
73 private final Context mContext;
74 private final PackageManager mPackageManager;
75 private final IPackageManager mIPackageManager;
76
77 public AppsRequestingAccessFetcher(Context context) {
78 mContext = context;
79 mPackageManager = context.getPackageManager();
80 mIPackageManager = ActivityThread.getPackageManager();
81 }
82
83 @Override
84 protected ArrayMap<String, PackageEntry> doInBackground(Void... params) {
85 final String[] packages;
86 try {
87 packages = mIPackageManager.getAppOpPermissionPackages(
88 Manifest.permission.PACKAGE_USAGE_STATS);
89 } catch (RemoteException e) {
90 Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting "
91 + Manifest.permission.PACKAGE_USAGE_STATS);
92 return null;
93 }
94
95 if (packages == null) {
96 // No packages are requesting permission to use the UsageStats API.
97 return null;
98 }
99
100 ArrayMap<String, PackageEntry> entries = new ArrayMap<>();
101 for (final String packageName : packages) {
102 if (!shouldIgnorePackage(packageName)) {
103 entries.put(packageName, new PackageEntry(packageName));
104 }
105 }
106
107 // Load the packages that have been granted the PACKAGE_USAGE_STATS permission.
108 final List<PackageInfo> packageInfos = mPackageManager.getPackagesHoldingPermissions(
109 PM_USAGE_STATS_PERMISSION, 0);
110 final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0;
111 for (int i = 0; i < packageInfoCount; i++) {
112 final PackageInfo packageInfo = packageInfos.get(i);
113 final PackageEntry pe = entries.get(packageInfo.packageName);
114 if (pe != null) {
115 pe.packageInfo = packageInfo;
116 pe.permissionGranted = true;
117 }
118 }
119
120 // Load the remaining packages that have requested but don't have the
121 // PACKAGE_USAGE_STATS permission.
122 int packageCount = entries.size();
123 for (int i = 0; i < packageCount; i++) {
124 final PackageEntry pe = entries.valueAt(i);
125 if (pe.packageInfo == null) {
126 try {
127 pe.packageInfo = mPackageManager.getPackageInfo(pe.packageName, 0);
128 } catch (PackageManager.NameNotFoundException e) {
129 // This package doesn't exist. This may occur when an app is uninstalled for
130 // one user, but it is not removed from the system.
131 entries.removeAt(i);
132 i--;
133 packageCount--;
134 }
135 }
136 }
137
138 // Find out which packages have been granted permission from AppOps.
139 final List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps(
140 APP_OPS_OP_CODES);
141 final int packageOpsCount = packageOps != null ? packageOps.size() : 0;
142 for (int i = 0; i < packageOpsCount; i++) {
143 final AppOpsManager.PackageOps packageOp = packageOps.get(i);
144 final PackageEntry pe = entries.get(packageOp.getPackageName());
145 if (pe == null) {
146 Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName()
147 + " but package doesn't exist or did not request UsageStats access");
148 continue;
149 }
150
151 if (packageOp.getOps().size() < 1) {
152 Log.w(TAG, "No AppOps permission exists for package "
153 + packageOp.getPackageName());
154 continue;
155 }
156
157 pe.appOpMode = packageOp.getOps().get(0).getMode();
158 }
159
160 return entries;
161 }
162
163 @Override
164 protected void onPostExecute(ArrayMap<String, PackageEntry> newEntries) {
165 mLastFetcherTask = null;
166
167 if (getActivity() == null) {
168 // We must have finished the Activity while we were processing in the background.
169 return;
170 }
171
172 if (newEntries == null) {
173 mPackageEntryMap.clear();
174 getPreferenceScreen().removeAll();
175 return;
176 }
177
178 // Find the deleted entries and remove them from the PreferenceScreen.
179 final int oldPackageCount = mPackageEntryMap.size();
180 for (int i = 0; i < oldPackageCount; i++) {
181 final PackageEntry oldPackageEntry = mPackageEntryMap.valueAt(i);
182 final PackageEntry newPackageEntry = newEntries.get(oldPackageEntry.packageName);
183 if (newPackageEntry == null) {
184 // This package has been removed.
185 getPreferenceScreen().removePreference(oldPackageEntry.preference);
186 } else {
187 // This package already exists in the preference hierarchy, so reuse that
188 // Preference.
189 newPackageEntry.preference = oldPackageEntry.preference;
190 }
191 }
192
193 // Now add new packages to the PreferenceScreen.
194 final int packageCount = newEntries.size();
195 for (int i = 0; i < packageCount; i++) {
196 final PackageEntry packageEntry = newEntries.valueAt(i);
197 if (packageEntry.preference == null) {
198 packageEntry.preference = new SwitchPreference(mContext);
199 packageEntry.preference.setPersistent(false);
200 packageEntry.preference.setOnPreferenceChangeListener(UsageAccessSettings.this);
201 getPreferenceScreen().addPreference(packageEntry.preference);
202 }
203 updatePreference(packageEntry);
204 }
205
206 mPackageEntryMap.clear();
207 mPackageEntryMap = newEntries;
208 }
209
210 private void updatePreference(PackageEntry pe) {
211 pe.preference.setIcon(pe.packageInfo.applicationInfo.loadIcon(mPackageManager));
212 pe.preference.setTitle(pe.packageInfo.applicationInfo.loadLabel(mPackageManager));
213 pe.preference.setKey(pe.packageName);
214
215 boolean check = false;
216 if (pe.appOpMode == AppOpsManager.MODE_ALLOWED) {
217 check = true;
218 } else if (pe.appOpMode == AppOpsManager.MODE_DEFAULT) {
219 // If the default AppOps mode is set, then fall back to
220 // whether the app has been granted permission by PackageManager.
221 check = pe.permissionGranted;
222 }
223
224 if (check != pe.preference.isChecked()) {
225 pe.preference.setChecked(check);
226 }
227 }
228 }
229
230 private static boolean shouldIgnorePackage(String packageName) {
231 return packageName.equals("android") || packageName.equals("com.android.settings");
232 }
233
234 private ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>();
235 private AppOpsManager mAppOpsManager;
236 private AppsRequestingAccessFetcher mLastFetcherTask;
237
238 @Override
239 public void onCreate(Bundle icicle) {
240 super.onCreate(icicle);
241
242 addPreferencesFromResource(R.xml.usage_access_settings);
243 getPreferenceScreen().setOrderingAsAdded(false);
244 mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
245 }
246
247 @Override
248 public void onResume() {
249 super.onResume();
250
251 updateInterestedApps();
252 mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false);
253 }
254
255 @Override
256 public void onPause() {
257 super.onPause();
258
259 mPackageMonitor.unregister();
260 if (mLastFetcherTask != null) {
261 mLastFetcherTask.cancel(true);
262 mLastFetcherTask = null;
263 }
264 }
265
266 private void updateInterestedApps() {
267 if (mLastFetcherTask != null) {
268 // Canceling can only fail for some obscure reason since mLastFetcherTask would be
269 // null if the task has already completed. So we ignore the result of cancel and
270 // spawn a new task to get fresh data. AsyncTask executes tasks serially anyways,
271 // so we are safe from running two tasks at the same time.
272 mLastFetcherTask.cancel(true);
273 }
274
275 mLastFetcherTask = new AppsRequestingAccessFetcher(getActivity());
276 mLastFetcherTask.execute();
277 }
278
279 @Override
280 public boolean onPreferenceChange(Preference preference, Object newValue) {
281 final String packageName = preference.getKey();
282 final PackageEntry pe = mPackageEntryMap.get(packageName);
283 if (pe == null) {
284 Log.w(TAG, "Preference change event for package " + packageName
285 + " but that package is no longer valid.");
286 return false;
287 }
288
289 if (!(newValue instanceof Boolean)) {
290 Log.w(TAG, "Preference change event for package " + packageName
291 + " had non boolean value of type " + newValue.getClass().getName());
292 return false;
293 }
294
295 final int newMode = (Boolean) newValue ?
296 AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
297 mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS, pe.packageInfo.applicationInfo.uid,
298 packageName, newMode);
299 pe.appOpMode = newMode;
300 return true;
301 }
302
303 private final PackageMonitor mPackageMonitor = new PackageMonitor() {
304 @Override
305 public void onPackageAdded(String packageName, int uid) {
306 updateInterestedApps();
307 }
308
309 @Override
310 public void onPackageRemoved(String packageName, int uid) {
311 updateInterestedApps();
312 }
313 };
314}