1 /* 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 17 package com.android.settings; 18 19 import com.android.internal.content.PackageMonitor; 20 21 import android.Manifest; 22 import android.app.ActivityThread; 23 import android.app.AlertDialog; 24 import android.app.AppOpsManager; 25 import android.app.Dialog; 26 import android.app.DialogFragment; 27 import android.app.Fragment; 28 import android.app.FragmentTransaction; 29 import android.content.Context; 30 import android.content.DialogInterface; 31 import android.content.pm.IPackageManager; 32 import android.content.pm.PackageInfo; 33 import android.content.pm.PackageManager; 34 import android.os.AsyncTask; 35 import android.os.Bundle; 36 import android.os.Looper; 37 import android.os.RemoteException; 38 import android.preference.Preference; 39 import android.preference.PreferenceScreen; 40 import android.preference.SwitchPreference; 41 import android.util.ArrayMap; 42 import android.util.Log; 43 44 import java.util.List; 45 46 public 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 { PackageEntry(String packageName)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 AppsRequestingAccessFetcher(Context context)84 public AppsRequestingAccessFetcher(Context context) { 85 mContext = context; 86 mPackageManager = context.getPackageManager(); 87 mIPackageManager = ActivityThread.getPackageManager(); 88 } 89 90 @Override doInBackground(Void... params)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 158 if (packageOp.getUid() != pe.packageInfo.applicationInfo.uid) { 159 // This AppOp does not belong to this user. 160 continue; 161 } 162 163 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 onPostExecute(ArrayMap<String, PackageEntry> newEntries)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(); 186 mPreferenceScreen.removeAll(); 187 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. 197 mPreferenceScreen.removePreference(oldPackageEntry.preference); 198 } 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); 213 mPreferenceScreen.addPreference(packageEntry.preference); 214 } 215 updatePreference(packageEntry); 216 } 217 218 mPackageEntryMap.clear(); 219 mPackageEntryMap = newEntries; 220 } 221 updatePreference(PackageEntry pe)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 shouldIgnorePackage(String packageName)242 static boolean shouldIgnorePackage(String packageName) { 243 return packageName.equals("android") || packageName.equals("com.android.settings"); 244 } 245 246 private AppsRequestingAccessFetcher mLastFetcherTask; 247 ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>(); 248 AppOpsManager mAppOpsManager; 249 PreferenceScreen mPreferenceScreen; 250 251 @Override onCreate(Bundle icicle)252 public void onCreate(Bundle icicle) { 253 super.onCreate(icicle); 254 255 addPreferencesFromResource(R.xml.usage_access_settings); 256 mPreferenceScreen = getPreferenceScreen(); 257 mPreferenceScreen.setOrderingAsAdded(false); 258 mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); 259 } 260 261 @Override onResume()262 public void onResume() { 263 super.onResume(); 264 265 updateInterestedApps(); 266 mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false); 267 } 268 269 @Override onPause()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 updateInterestedApps()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 onPreferenceChange(Preference preference, Object newValue)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; 311 312 // Check if we need to do any work. 313 if (pe.appOpMode != newMode) { 314 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. 321 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"); 327 return false; 328 } 329 return true; 330 } 331 setNewMode(PackageEntry pe, int newMode)332 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 allowAccess(String packageName)338 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 349 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 }; 360 361 public static class WarningDialogFragment extends DialogFragment 362 implements DialogInterface.OnClickListener { 363 private static final String ARG_PACKAGE_NAME = "package"; 364 newInstance(String packageName)365 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; 371 } 372 373 @Override onCreateDialog(Bundle savedInstanceState)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) 380 .setPositiveButton(android.R.string.ok, this) 381 .create(); 382 } 383 384 @Override onClick(DialogInterface dialog, int which)385 public void onClick(DialogInterface dialog, int which) { 386 if (which == DialogInterface.BUTTON_POSITIVE) { 387 ((UsageAccessSettings) getParentFragment()).allowAccess( 388 getArguments().getString(ARG_PACKAGE_NAME)); 389 } else { 390 dialog.cancel(); 391 } 392 } 393 } 394 } 395