Home | History | Annotate | Download | only in settings
      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 {
     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 
    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
    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 
    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 
    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
    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
    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;
    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 
    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 
    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 
    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
    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
    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