OSDN Git Service

Implement the Usage Access Settings UI
authorAdam Lesinski <adamlesinski@google.com>
Tue, 22 Jul 2014 17:01:08 +0000 (10:01 -0700)
committerAdam Lesinski <adamlesinski@google.com>
Thu, 24 Jul 2014 20:24:19 +0000 (13:24 -0700)
Scans for apps requesting permission to use the
UsageStats API and displays them so that the user
can grant or deny access.

Bug: 16461070

Change-Id: Ie9f611688581cdd60ffe9f59e566f658ac2564e9

res/values/strings.xml
res/xml/security_settings_misc.xml
res/xml/usage_access_settings.xml [new file with mode: 0644]
src/com/android/settings/UsageAccessSettings.java [new file with mode: 0644]

index 92d8420..02511b3 100644 (file)
     <!-- Description of dialog to explain that a lock screen password is required to use credential storage [CHAR LIMIT=NONE] -->
     <string name="credentials_configure_lock_screen_hint">You need to set a lock screen PIN or password before you can use credential storage.</string>
 
+    <!-- Title of Usage Access preference item [CHAR LIMIT=30] -->
+    <string name="usage_access_title">Usage access</string>
+    <!-- Summary of Usage Access preference item [CHAR LIMIT=80] -->
+    <string name="usage_access_summary">Apps that have access to your device\'s usage history.</string>
+
     <!-- Sound settings screen, setting check box label -->
     <string name="emergency_tone_title">Emergency tone</string>
     <!-- Sound settings screen, setting option summary text -->
index 1fa8b9a..06743a5 100644 (file)
                 android:summary="@string/switch_off_text"
                 android:fragment="com.android.settings.ScreenPinningSettings"/>
 
+        <Preference android:key="usage_access"
+                    android:title="@string/usage_access_title"
+                    android:summary="@string/usage_access_summary"
+                    android:fragment="com.android.settings.UsageAccessSettings"/>
+
     </PreferenceCategory>
 
 </PreferenceScreen>
diff --git a/res/xml/usage_access_settings.xml b/res/xml/usage_access_settings.xml
new file mode 100644 (file)
index 0000000..9cea725
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="usage_access"
+    android:title="@string/usage_access_title" />
diff --git a/src/com/android/settings/UsageAccessSettings.java b/src/com/android/settings/UsageAccessSettings.java
new file mode 100644 (file)
index 0000000..8e8e533
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings;
+
+import com.android.internal.content.PackageMonitor;
+
+import android.Manifest;
+import android.app.ActivityThread;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.util.List;
+
+public class UsageAccessSettings extends SettingsPreferenceFragment implements
+        Preference.OnPreferenceChangeListener {
+
+    private static final String TAG = "UsageAccessSettings";
+
+    private static final String[] PM_USAGE_STATS_PERMISSION = new String[] {
+            Manifest.permission.PACKAGE_USAGE_STATS
+    };
+
+    private static final int[] APP_OPS_OP_CODES = new int[] {
+            AppOpsManager.OP_GET_USAGE_STATS
+    };
+
+    private static class PackageEntry {
+        public PackageEntry(String packageName) {
+            this.packageName = packageName;
+            this.appOpMode = AppOpsManager.MODE_DEFAULT;
+        }
+
+        final String packageName;
+        PackageInfo packageInfo;
+        boolean permissionGranted;
+        int appOpMode;
+
+        SwitchPreference preference;
+    }
+
+    /**
+     * Fetches the list of Apps that are requesting access to the UsageStats API and updates
+     * the PreferenceScreen with the results when complete.
+     */
+    private class AppsRequestingAccessFetcher extends
+            AsyncTask<Void, Void, ArrayMap<String, PackageEntry>> {
+
+        private final Context mContext;
+        private final PackageManager mPackageManager;
+        private final IPackageManager mIPackageManager;
+
+        public AppsRequestingAccessFetcher(Context context) {
+            mContext = context;
+            mPackageManager = context.getPackageManager();
+            mIPackageManager = ActivityThread.getPackageManager();
+        }
+
+        @Override
+        protected ArrayMap<String, PackageEntry> doInBackground(Void... params) {
+            final String[] packages;
+            try {
+                packages = mIPackageManager.getAppOpPermissionPackages(
+                        Manifest.permission.PACKAGE_USAGE_STATS);
+            } catch (RemoteException e) {
+                Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting "
+                        + Manifest.permission.PACKAGE_USAGE_STATS);
+                return null;
+            }
+
+            if (packages == null) {
+                // No packages are requesting permission to use the UsageStats API.
+                return null;
+            }
+
+            ArrayMap<String, PackageEntry> entries = new ArrayMap<>();
+            for (final String packageName : packages) {
+                if (!shouldIgnorePackage(packageName)) {
+                    entries.put(packageName, new PackageEntry(packageName));
+                }
+            }
+
+             // Load the packages that have been granted the PACKAGE_USAGE_STATS permission.
+            final List<PackageInfo> packageInfos = mPackageManager.getPackagesHoldingPermissions(
+                    PM_USAGE_STATS_PERMISSION, 0);
+            final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0;
+            for (int i = 0; i < packageInfoCount; i++) {
+                final PackageInfo packageInfo = packageInfos.get(i);
+                final PackageEntry pe = entries.get(packageInfo.packageName);
+                if (pe != null) {
+                    pe.packageInfo = packageInfo;
+                    pe.permissionGranted = true;
+                }
+            }
+
+            // Load the remaining packages that have requested but don't have the
+            // PACKAGE_USAGE_STATS permission.
+            int packageCount = entries.size();
+            for (int i = 0; i < packageCount; i++) {
+                final PackageEntry pe = entries.valueAt(i);
+                if (pe.packageInfo == null) {
+                    try {
+                        pe.packageInfo = mPackageManager.getPackageInfo(pe.packageName, 0);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        // This package doesn't exist. This may occur when an app is uninstalled for
+                        // one user, but it is not removed from the system.
+                        entries.removeAt(i);
+                        i--;
+                        packageCount--;
+                    }
+                }
+            }
+
+            // Find out which packages have been granted permission from AppOps.
+            final List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps(
+                    APP_OPS_OP_CODES);
+            final int packageOpsCount = packageOps != null ? packageOps.size() : 0;
+            for (int i = 0; i < packageOpsCount; i++) {
+                final AppOpsManager.PackageOps packageOp = packageOps.get(i);
+                final PackageEntry pe = entries.get(packageOp.getPackageName());
+                if (pe == null) {
+                    Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName()
+                            + " but package doesn't exist or did not request UsageStats access");
+                    continue;
+                }
+
+                if (packageOp.getOps().size() < 1) {
+                    Log.w(TAG, "No AppOps permission exists for package "
+                            + packageOp.getPackageName());
+                    continue;
+                }
+
+                pe.appOpMode = packageOp.getOps().get(0).getMode();
+            }
+
+            return entries;
+        }
+
+        @Override
+        protected void onPostExecute(ArrayMap<String, PackageEntry> newEntries) {
+            mLastFetcherTask = null;
+
+            if (getActivity() == null) {
+                // We must have finished the Activity while we were processing in the background.
+                return;
+            }
+
+            if (newEntries == null) {
+                mPackageEntryMap.clear();
+                getPreferenceScreen().removeAll();
+                return;
+            }
+
+            // Find the deleted entries and remove them from the PreferenceScreen.
+            final int oldPackageCount = mPackageEntryMap.size();
+            for (int i = 0; i < oldPackageCount; i++) {
+                final PackageEntry oldPackageEntry = mPackageEntryMap.valueAt(i);
+                final PackageEntry newPackageEntry = newEntries.get(oldPackageEntry.packageName);
+                if (newPackageEntry == null) {
+                    // This package has been removed.
+                    getPreferenceScreen().removePreference(oldPackageEntry.preference);
+                } else {
+                    // This package already exists in the preference hierarchy, so reuse that
+                    // Preference.
+                    newPackageEntry.preference = oldPackageEntry.preference;
+                }
+            }
+
+            // Now add new packages to the PreferenceScreen.
+            final int packageCount = newEntries.size();
+            for (int i = 0; i < packageCount; i++) {
+                final PackageEntry packageEntry = newEntries.valueAt(i);
+                if (packageEntry.preference == null) {
+                    packageEntry.preference = new SwitchPreference(mContext);
+                    packageEntry.preference.setPersistent(false);
+                    packageEntry.preference.setOnPreferenceChangeListener(UsageAccessSettings.this);
+                    getPreferenceScreen().addPreference(packageEntry.preference);
+                }
+                updatePreference(packageEntry);
+            }
+
+            mPackageEntryMap.clear();
+            mPackageEntryMap = newEntries;
+        }
+
+        private void updatePreference(PackageEntry pe) {
+            pe.preference.setIcon(pe.packageInfo.applicationInfo.loadIcon(mPackageManager));
+            pe.preference.setTitle(pe.packageInfo.applicationInfo.loadLabel(mPackageManager));
+            pe.preference.setKey(pe.packageName);
+
+            boolean check = false;
+            if (pe.appOpMode == AppOpsManager.MODE_ALLOWED) {
+                check = true;
+            } else if (pe.appOpMode == AppOpsManager.MODE_DEFAULT) {
+                // If the default AppOps mode is set, then fall back to
+                // whether the app has been granted permission by PackageManager.
+                check = pe.permissionGranted;
+            }
+
+            if (check != pe.preference.isChecked()) {
+                pe.preference.setChecked(check);
+            }
+        }
+    }
+
+    private static boolean shouldIgnorePackage(String packageName) {
+        return packageName.equals("android") || packageName.equals("com.android.settings");
+    }
+
+    private ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>();
+    private AppOpsManager mAppOpsManager;
+    private AppsRequestingAccessFetcher mLastFetcherTask;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        addPreferencesFromResource(R.xml.usage_access_settings);
+        getPreferenceScreen().setOrderingAsAdded(false);
+        mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        updateInterestedApps();
+        mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+
+        mPackageMonitor.unregister();
+        if (mLastFetcherTask != null) {
+            mLastFetcherTask.cancel(true);
+            mLastFetcherTask = null;
+        }
+    }
+
+    private void updateInterestedApps() {
+        if (mLastFetcherTask != null) {
+            // Canceling can only fail for some obscure reason since mLastFetcherTask would be
+            // null if the task has already completed. So we ignore the result of cancel and
+            // spawn a new task to get fresh data. AsyncTask executes tasks serially anyways,
+            // so we are safe from running two tasks at the same time.
+            mLastFetcherTask.cancel(true);
+        }
+
+        mLastFetcherTask = new AppsRequestingAccessFetcher(getActivity());
+        mLastFetcherTask.execute();
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        final String packageName = preference.getKey();
+        final PackageEntry pe = mPackageEntryMap.get(packageName);
+        if (pe == null) {
+            Log.w(TAG, "Preference change event for package " + packageName
+                    + " but that package is no longer valid.");
+            return false;
+        }
+
+        if (!(newValue instanceof Boolean)) {
+            Log.w(TAG, "Preference change event for package " + packageName
+                    + " had non boolean value of type " + newValue.getClass().getName());
+            return false;
+        }
+
+        final int newMode = (Boolean) newValue ?
+                AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
+        mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS, pe.packageInfo.applicationInfo.uid,
+                packageName, newMode);
+        pe.appOpMode = newMode;
+        return true;
+    }
+
+    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
+        @Override
+        public void onPackageAdded(String packageName, int uid) {
+            updateInterestedApps();
+        }
+
+        @Override
+        public void onPackageRemoved(String packageName, int uid) {
+            updateInterestedApps();
+        }
+    };
+}