blob: 8ae277d228d3740247931c16c4138c9737ca4000 [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
Adam Lesinski366e7a22014-07-25 10:20:26 -0700151 if (packageOp.getUid() != pe.packageInfo.applicationInfo.uid) {
152 // This AppOp does not belong to this user.
153 continue;
154 }
155
Adam Lesinski468d3912014-07-22 10:01:08 -0700156 if (packageOp.getOps().size() < 1) {
157 Log.w(TAG, "No AppOps permission exists for package "
158 + packageOp.getPackageName());
159 continue;
160 }
161
162 pe.appOpMode = packageOp.getOps().get(0).getMode();
163 }
164
165 return entries;
166 }
167
168 @Override
169 protected void onPostExecute(ArrayMap<String, PackageEntry> newEntries) {
170 mLastFetcherTask = null;
171
172 if (getActivity() == null) {
173 // We must have finished the Activity while we were processing in the background.
174 return;
175 }
176
177 if (newEntries == null) {
178 mPackageEntryMap.clear();
179 getPreferenceScreen().removeAll();
180 return;
181 }
182
183 // Find the deleted entries and remove them from the PreferenceScreen.
184 final int oldPackageCount = mPackageEntryMap.size();
185 for (int i = 0; i < oldPackageCount; i++) {
186 final PackageEntry oldPackageEntry = mPackageEntryMap.valueAt(i);
187 final PackageEntry newPackageEntry = newEntries.get(oldPackageEntry.packageName);
188 if (newPackageEntry == null) {
189 // This package has been removed.
190 getPreferenceScreen().removePreference(oldPackageEntry.preference);
191 } else {
192 // This package already exists in the preference hierarchy, so reuse that
193 // Preference.
194 newPackageEntry.preference = oldPackageEntry.preference;
195 }
196 }
197
198 // Now add new packages to the PreferenceScreen.
199 final int packageCount = newEntries.size();
200 for (int i = 0; i < packageCount; i++) {
201 final PackageEntry packageEntry = newEntries.valueAt(i);
202 if (packageEntry.preference == null) {
203 packageEntry.preference = new SwitchPreference(mContext);
204 packageEntry.preference.setPersistent(false);
205 packageEntry.preference.setOnPreferenceChangeListener(UsageAccessSettings.this);
206 getPreferenceScreen().addPreference(packageEntry.preference);
207 }
208 updatePreference(packageEntry);
209 }
210
211 mPackageEntryMap.clear();
212 mPackageEntryMap = newEntries;
213 }
214
215 private void updatePreference(PackageEntry pe) {
216 pe.preference.setIcon(pe.packageInfo.applicationInfo.loadIcon(mPackageManager));
217 pe.preference.setTitle(pe.packageInfo.applicationInfo.loadLabel(mPackageManager));
218 pe.preference.setKey(pe.packageName);
219
220 boolean check = false;
221 if (pe.appOpMode == AppOpsManager.MODE_ALLOWED) {
222 check = true;
223 } else if (pe.appOpMode == AppOpsManager.MODE_DEFAULT) {
224 // If the default AppOps mode is set, then fall back to
225 // whether the app has been granted permission by PackageManager.
226 check = pe.permissionGranted;
227 }
228
229 if (check != pe.preference.isChecked()) {
230 pe.preference.setChecked(check);
231 }
232 }
233 }
234
235 private static boolean shouldIgnorePackage(String packageName) {
236 return packageName.equals("android") || packageName.equals("com.android.settings");
237 }
238
239 private ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>();
240 private AppOpsManager mAppOpsManager;
241 private AppsRequestingAccessFetcher mLastFetcherTask;
242
243 @Override
244 public void onCreate(Bundle icicle) {
245 super.onCreate(icicle);
246
247 addPreferencesFromResource(R.xml.usage_access_settings);
248 getPreferenceScreen().setOrderingAsAdded(false);
249 mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
250 }
251
252 @Override
253 public void onResume() {
254 super.onResume();
255
256 updateInterestedApps();
257 mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false);
258 }
259
260 @Override
261 public void onPause() {
262 super.onPause();
263
264 mPackageMonitor.unregister();
265 if (mLastFetcherTask != null) {
266 mLastFetcherTask.cancel(true);
267 mLastFetcherTask = null;
268 }
269 }
270
271 private void updateInterestedApps() {
272 if (mLastFetcherTask != null) {
273 // Canceling can only fail for some obscure reason since mLastFetcherTask would be
274 // null if the task has already completed. So we ignore the result of cancel and
275 // spawn a new task to get fresh data. AsyncTask executes tasks serially anyways,
276 // so we are safe from running two tasks at the same time.
277 mLastFetcherTask.cancel(true);
278 }
279
280 mLastFetcherTask = new AppsRequestingAccessFetcher(getActivity());
281 mLastFetcherTask.execute();
282 }
283
284 @Override
285 public boolean onPreferenceChange(Preference preference, Object newValue) {
286 final String packageName = preference.getKey();
287 final PackageEntry pe = mPackageEntryMap.get(packageName);
288 if (pe == null) {
289 Log.w(TAG, "Preference change event for package " + packageName
290 + " but that package is no longer valid.");
291 return false;
292 }
293
294 if (!(newValue instanceof Boolean)) {
295 Log.w(TAG, "Preference change event for package " + packageName
296 + " had non boolean value of type " + newValue.getClass().getName());
297 return false;
298 }
299
300 final int newMode = (Boolean) newValue ?
301 AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
Adam Lesinski366e7a22014-07-25 10:20:26 -0700302
303 // Check if we need to do any work.
304 if (pe.appOpMode != newMode) {
305 mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS,
306 pe.packageInfo.applicationInfo.uid, packageName, newMode);
307 pe.appOpMode = newMode;
308 }
Adam Lesinski468d3912014-07-22 10:01:08 -0700309 return true;
310 }
311
312 private final PackageMonitor mPackageMonitor = new PackageMonitor() {
313 @Override
314 public void onPackageAdded(String packageName, int uid) {
315 updateInterestedApps();
316 }
317
318 @Override
319 public void onPackageRemoved(String packageName, int uid) {
320 updateInterestedApps();
321 }
322 };
323}