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