blob: 89e184e34a0f736a4ea73972779eb5534b3487c7 [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;
Adam Lesinskie01dfd12014-08-11 20:54:12 -070023import android.app.AlertDialog;
Adam Lesinski468d3912014-07-22 10:01:08 -070024import android.app.AppOpsManager;
Adam Lesinskie01dfd12014-08-11 20:54:12 -070025import android.app.Dialog;
26import android.app.DialogFragment;
Adam Lesinski21dfa202014-09-09 11:34:55 -070027import android.app.Fragment;
28import android.app.FragmentTransaction;
Adam Lesinski468d3912014-07-22 10:01:08 -070029import android.content.Context;
Adam Lesinskie01dfd12014-08-11 20:54:12 -070030import android.content.DialogInterface;
Adam Lesinski468d3912014-07-22 10:01:08 -070031import android.content.pm.IPackageManager;
32import android.content.pm.PackageInfo;
33import android.content.pm.PackageManager;
34import android.os.AsyncTask;
35import android.os.Bundle;
36import android.os.Looper;
37import android.os.RemoteException;
38import android.preference.Preference;
Adam Lesinski1813c622014-08-27 19:00:30 -070039import android.preference.PreferenceScreen;
Adam Lesinski468d3912014-07-22 10:01:08 -070040import android.preference.SwitchPreference;
41import android.util.ArrayMap;
42import android.util.Log;
43
44import java.util.List;
45
46public class UsageAccessSettings extends SettingsPreferenceFragment implements
47 Preference.OnPreferenceChangeListener {
48
49 private static final String TAG = "UsageAccessSettings";
50
51 private static final String[] PM_USAGE_STATS_PERMISSION = new String[] {
52 Manifest.permission.PACKAGE_USAGE_STATS
53 };
54
55 private static final int[] APP_OPS_OP_CODES = new int[] {
56 AppOpsManager.OP_GET_USAGE_STATS
57 };
58
59 private static class PackageEntry {
60 public PackageEntry(String packageName) {
61 this.packageName = packageName;
62 this.appOpMode = AppOpsManager.MODE_DEFAULT;
63 }
64
65 final String packageName;
66 PackageInfo packageInfo;
67 boolean permissionGranted;
68 int appOpMode;
69
70 SwitchPreference preference;
71 }
72
73 /**
74 * Fetches the list of Apps that are requesting access to the UsageStats API and updates
75 * the PreferenceScreen with the results when complete.
76 */
77 private class AppsRequestingAccessFetcher extends
78 AsyncTask<Void, Void, ArrayMap<String, PackageEntry>> {
79
80 private final Context mContext;
81 private final PackageManager mPackageManager;
82 private final IPackageManager mIPackageManager;
83
84 public AppsRequestingAccessFetcher(Context context) {
85 mContext = context;
86 mPackageManager = context.getPackageManager();
87 mIPackageManager = ActivityThread.getPackageManager();
88 }
89
90 @Override
91 protected ArrayMap<String, PackageEntry> doInBackground(Void... params) {
92 final String[] packages;
93 try {
94 packages = mIPackageManager.getAppOpPermissionPackages(
95 Manifest.permission.PACKAGE_USAGE_STATS);
96 } catch (RemoteException e) {
97 Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting "
98 + Manifest.permission.PACKAGE_USAGE_STATS);
99 return null;
100 }
101
102 if (packages == null) {
103 // No packages are requesting permission to use the UsageStats API.
104 return null;
105 }
106
107 ArrayMap<String, PackageEntry> entries = new ArrayMap<>();
108 for (final String packageName : packages) {
109 if (!shouldIgnorePackage(packageName)) {
110 entries.put(packageName, new PackageEntry(packageName));
111 }
112 }
113
114 // Load the packages that have been granted the PACKAGE_USAGE_STATS permission.
115 final List<PackageInfo> packageInfos = mPackageManager.getPackagesHoldingPermissions(
116 PM_USAGE_STATS_PERMISSION, 0);
117 final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0;
118 for (int i = 0; i < packageInfoCount; i++) {
119 final PackageInfo packageInfo = packageInfos.get(i);
120 final PackageEntry pe = entries.get(packageInfo.packageName);
121 if (pe != null) {
122 pe.packageInfo = packageInfo;
123 pe.permissionGranted = true;
124 }
125 }
126
127 // Load the remaining packages that have requested but don't have the
128 // PACKAGE_USAGE_STATS permission.
129 int packageCount = entries.size();
130 for (int i = 0; i < packageCount; i++) {
131 final PackageEntry pe = entries.valueAt(i);
132 if (pe.packageInfo == null) {
133 try {
134 pe.packageInfo = mPackageManager.getPackageInfo(pe.packageName, 0);
135 } catch (PackageManager.NameNotFoundException e) {
136 // This package doesn't exist. This may occur when an app is uninstalled for
137 // one user, but it is not removed from the system.
138 entries.removeAt(i);
139 i--;
140 packageCount--;
141 }
142 }
143 }
144
145 // Find out which packages have been granted permission from AppOps.
146 final List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps(
147 APP_OPS_OP_CODES);
148 final int packageOpsCount = packageOps != null ? packageOps.size() : 0;
149 for (int i = 0; i < packageOpsCount; i++) {
150 final AppOpsManager.PackageOps packageOp = packageOps.get(i);
151 final PackageEntry pe = entries.get(packageOp.getPackageName());
152 if (pe == null) {
153 Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName()
154 + " but package doesn't exist or did not request UsageStats access");
155 continue;
156 }
157
Adam Lesinski366e7a22014-07-25 10:20:26 -0700158 if (packageOp.getUid() != pe.packageInfo.applicationInfo.uid) {
159 // This AppOp does not belong to this user.
160 continue;
161 }
162
Adam Lesinski468d3912014-07-22 10:01:08 -0700163 if (packageOp.getOps().size() < 1) {
164 Log.w(TAG, "No AppOps permission exists for package "
165 + packageOp.getPackageName());
166 continue;
167 }
168
169 pe.appOpMode = packageOp.getOps().get(0).getMode();
170 }
171
172 return entries;
173 }
174
175 @Override
176 protected void onPostExecute(ArrayMap<String, PackageEntry> newEntries) {
177 mLastFetcherTask = null;
178
179 if (getActivity() == null) {
180 // We must have finished the Activity while we were processing in the background.
181 return;
182 }
183
184 if (newEntries == null) {
185 mPackageEntryMap.clear();
Adam Lesinski1813c622014-08-27 19:00:30 -0700186 mPreferenceScreen.removeAll();
Adam Lesinski468d3912014-07-22 10:01:08 -0700187 return;
188 }
189
190 // Find the deleted entries and remove them from the PreferenceScreen.
191 final int oldPackageCount = mPackageEntryMap.size();
192 for (int i = 0; i < oldPackageCount; i++) {
193 final PackageEntry oldPackageEntry = mPackageEntryMap.valueAt(i);
194 final PackageEntry newPackageEntry = newEntries.get(oldPackageEntry.packageName);
195 if (newPackageEntry == null) {
196 // This package has been removed.
Adam Lesinski1813c622014-08-27 19:00:30 -0700197 mPreferenceScreen.removePreference(oldPackageEntry.preference);
Adam Lesinski468d3912014-07-22 10:01:08 -0700198 } else {
199 // This package already exists in the preference hierarchy, so reuse that
200 // Preference.
201 newPackageEntry.preference = oldPackageEntry.preference;
202 }
203 }
204
205 // Now add new packages to the PreferenceScreen.
206 final int packageCount = newEntries.size();
207 for (int i = 0; i < packageCount; i++) {
208 final PackageEntry packageEntry = newEntries.valueAt(i);
209 if (packageEntry.preference == null) {
210 packageEntry.preference = new SwitchPreference(mContext);
211 packageEntry.preference.setPersistent(false);
212 packageEntry.preference.setOnPreferenceChangeListener(UsageAccessSettings.this);
Adam Lesinski1813c622014-08-27 19:00:30 -0700213 mPreferenceScreen.addPreference(packageEntry.preference);
Adam Lesinski468d3912014-07-22 10:01:08 -0700214 }
215 updatePreference(packageEntry);
216 }
217
218 mPackageEntryMap.clear();
219 mPackageEntryMap = newEntries;
220 }
221
222 private void updatePreference(PackageEntry pe) {
223 pe.preference.setIcon(pe.packageInfo.applicationInfo.loadIcon(mPackageManager));
224 pe.preference.setTitle(pe.packageInfo.applicationInfo.loadLabel(mPackageManager));
225 pe.preference.setKey(pe.packageName);
226
227 boolean check = false;
228 if (pe.appOpMode == AppOpsManager.MODE_ALLOWED) {
229 check = true;
230 } else if (pe.appOpMode == AppOpsManager.MODE_DEFAULT) {
231 // If the default AppOps mode is set, then fall back to
232 // whether the app has been granted permission by PackageManager.
233 check = pe.permissionGranted;
234 }
235
236 if (check != pe.preference.isChecked()) {
237 pe.preference.setChecked(check);
238 }
239 }
240 }
241
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700242 static boolean shouldIgnorePackage(String packageName) {
Adam Lesinski468d3912014-07-22 10:01:08 -0700243 return packageName.equals("android") || packageName.equals("com.android.settings");
244 }
245
Adam Lesinski468d3912014-07-22 10:01:08 -0700246 private AppsRequestingAccessFetcher mLastFetcherTask;
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700247 ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>();
248 AppOpsManager mAppOpsManager;
Adam Lesinski1813c622014-08-27 19:00:30 -0700249 PreferenceScreen mPreferenceScreen;
Adam Lesinski468d3912014-07-22 10:01:08 -0700250
251 @Override
252 public void onCreate(Bundle icicle) {
253 super.onCreate(icicle);
254
255 addPreferencesFromResource(R.xml.usage_access_settings);
Adam Lesinski1813c622014-08-27 19:00:30 -0700256 mPreferenceScreen = getPreferenceScreen();
257 mPreferenceScreen.setOrderingAsAdded(false);
Adam Lesinski468d3912014-07-22 10:01:08 -0700258 mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
259 }
260
261 @Override
262 public void onResume() {
263 super.onResume();
264
265 updateInterestedApps();
266 mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false);
267 }
268
269 @Override
270 public void onPause() {
271 super.onPause();
272
273 mPackageMonitor.unregister();
274 if (mLastFetcherTask != null) {
275 mLastFetcherTask.cancel(true);
276 mLastFetcherTask = null;
277 }
278 }
279
280 private void updateInterestedApps() {
281 if (mLastFetcherTask != null) {
282 // Canceling can only fail for some obscure reason since mLastFetcherTask would be
283 // null if the task has already completed. So we ignore the result of cancel and
284 // spawn a new task to get fresh data. AsyncTask executes tasks serially anyways,
285 // so we are safe from running two tasks at the same time.
286 mLastFetcherTask.cancel(true);
287 }
288
289 mLastFetcherTask = new AppsRequestingAccessFetcher(getActivity());
290 mLastFetcherTask.execute();
291 }
292
293 @Override
294 public boolean onPreferenceChange(Preference preference, Object newValue) {
295 final String packageName = preference.getKey();
296 final PackageEntry pe = mPackageEntryMap.get(packageName);
297 if (pe == null) {
298 Log.w(TAG, "Preference change event for package " + packageName
299 + " but that package is no longer valid.");
300 return false;
301 }
302
303 if (!(newValue instanceof Boolean)) {
304 Log.w(TAG, "Preference change event for package " + packageName
305 + " had non boolean value of type " + newValue.getClass().getName());
306 return false;
307 }
308
309 final int newMode = (Boolean) newValue ?
310 AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
Adam Lesinski366e7a22014-07-25 10:20:26 -0700311
312 // Check if we need to do any work.
313 if (pe.appOpMode != newMode) {
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700314 if (newMode != AppOpsManager.MODE_ALLOWED) {
315 // Turning off the setting has no warning.
316 setNewMode(pe, newMode);
317 return true;
318 }
319
320 // Turning on the setting has a Warning.
Adam Lesinski21dfa202014-09-09 11:34:55 -0700321 FragmentTransaction ft = getChildFragmentManager().beginTransaction();
322 Fragment prev = getChildFragmentManager().findFragmentByTag("warning");
323 if (prev != null) {
324 ft.remove(prev);
325 }
326 WarningDialogFragment.newInstance(pe.packageName).show(ft, "warning");
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700327 return false;
Adam Lesinski366e7a22014-07-25 10:20:26 -0700328 }
Adam Lesinski468d3912014-07-22 10:01:08 -0700329 return true;
330 }
331
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700332 void setNewMode(PackageEntry pe, int newMode) {
333 mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS,
334 pe.packageInfo.applicationInfo.uid, pe.packageName, newMode);
335 pe.appOpMode = newMode;
336 }
337
Adam Lesinski21dfa202014-09-09 11:34:55 -0700338 void allowAccess(String packageName) {
339 final PackageEntry entry = mPackageEntryMap.get(packageName);
340 if (entry == null) {
341 Log.w(TAG, "Unable to give access to package " + packageName + ": it does not exist.");
342 return;
343 }
344
345 setNewMode(entry, AppOpsManager.MODE_ALLOWED);
346 entry.preference.setChecked(true);
347 }
348
Adam Lesinski468d3912014-07-22 10:01:08 -0700349 private final PackageMonitor mPackageMonitor = new PackageMonitor() {
350 @Override
351 public void onPackageAdded(String packageName, int uid) {
352 updateInterestedApps();
353 }
354
355 @Override
356 public void onPackageRemoved(String packageName, int uid) {
357 updateInterestedApps();
358 }
359 };
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700360
Adam Lesinski21dfa202014-09-09 11:34:55 -0700361 public static class WarningDialogFragment extends DialogFragment
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700362 implements DialogInterface.OnClickListener {
Adam Lesinski21dfa202014-09-09 11:34:55 -0700363 private static final String ARG_PACKAGE_NAME = "package";
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700364
Adam Lesinski21dfa202014-09-09 11:34:55 -0700365 public static WarningDialogFragment newInstance(String packageName) {
366 WarningDialogFragment dialog = new WarningDialogFragment();
367 Bundle args = new Bundle();
368 args.putString(ARG_PACKAGE_NAME, packageName);
369 dialog.setArguments(args);
370 return dialog;
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700371 }
372
373 @Override
374 public Dialog onCreateDialog(Bundle savedInstanceState) {
375 return new AlertDialog.Builder(getActivity())
376 .setTitle(R.string.allow_usage_access_title)
377 .setMessage(R.string.allow_usage_access_message)
378 .setIconAttribute(android.R.attr.alertDialogIcon)
379 .setNegativeButton(R.string.cancel, this)
Adam Lesinski1813c622014-08-27 19:00:30 -0700380 .setPositiveButton(android.R.string.ok, this)
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700381 .create();
382 }
383
384 @Override
385 public void onClick(DialogInterface dialog, int which) {
386 if (which == DialogInterface.BUTTON_POSITIVE) {
Adam Lesinski21dfa202014-09-09 11:34:55 -0700387 ((UsageAccessSettings) getParentFragment()).allowAccess(
388 getArguments().getString(ARG_PACKAGE_NAME));
Adam Lesinskie01dfd12014-08-11 20:54:12 -0700389 } else {
390 dialog.cancel();
391 }
392 }
393 }
Adam Lesinski468d3912014-07-22 10:01:08 -0700394}