Add Usage Access control

Bug: 63672290
Test: Settings -> Apps -> Usage Access

Change-Id: I0cc24e2040cf6307e55913720871b0b573fcea53
(cherry picked from commit db8bb1b704d51343b09e24a9c03d8129d5272767)
(cherry picked from commit 6e908bc33779e9113d240993212238640cc5e61f)
diff --git a/Settings/res/values/strings.xml b/Settings/res/values/strings.xml
index 5f8d40c..a546dd0 100644
--- a/Settings/res/values/strings.xml
+++ b/Settings/res/values/strings.xml
@@ -1451,4 +1451,15 @@
     <string name="available_virtual_keyboard_category">Available virtual keyboards</string>
     <!-- Title for the preference entry for managing keyboards [CHAR LIMIT=50] -->
     <string name="manage_keyboards">Manage keyboards</string>
+
+    <!-- Preference summary text for an app when it is allowed for a permission. [CHAR LIMIT=45] -->
+    <string name="app_permission_summary_allowed">Allowed</string>
+    <!-- Preference summary text for an app when it is disallowed for a permission. [CHAR LIMIT=45] -->
+    <string name="app_permission_summary_not_allowed">Not allowed</string>
+
+    <!-- Title of usage access screen [CHAR LIMIT=30] -->
+    <string name="usage_access">Usage access</string>
+    <!-- Description of the usage access setting [CHAR LIMIT=NONE] -->
+    <string name="usage_access_description">Usage access allows an app to track what other apps you\u2019re using and how often, as well as your carrier, language settings, and other details.</string>
+
 </resources>
diff --git a/Settings/res/xml/app_usage_access.xml b/Settings/res/xml/app_usage_access.xml
new file mode 100644
index 0000000..6b1c461
--- /dev/null
+++ b/Settings/res/xml/app_usage_access.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 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:title="@string/usage_access">
+    <Preference
+            android:key="header"
+            android:title="@string/usage_access_description"
+            android:singleLineTitle="false"/>
+</PreferenceScreen>
diff --git a/Settings/res/xml/apps.xml b/Settings/res/xml/apps.xml
new file mode 100644
index 0000000..e462b55
--- /dev/null
+++ b/Settings/res/xml/apps.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 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:title="@string/device_apps">
+    <Preference
+            android:key="Permissions"
+            android:title="@string/device_apps_permissions">
+        <intent android:action="android.intent.action.MANAGE_PERMISSIONS"/>
+    </Preference>
+    <Preference
+            android:key="AppUsage"
+            android:title="@string/usage_access"
+            android:fragment="com.android.tv.settings.device.apps.specialaccess.ManageAppUsageAccess"/>
+    <PreferenceCategory
+            android:key="DownloadedPreferenceGroup"
+            android:title="@string/apps_downloaded"
+            android:orderingFromXml="false"/>
+    <PreferenceCategory
+            android:key="SystemPreferenceGroup"
+            android:title="@string/apps_system"
+            android:orderingFromXml="false"/>
+</PreferenceScreen>
diff --git a/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java b/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java
index 50bd405..cd799e0 100644
--- a/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java
+++ b/Settings/src/com/android/tv/settings/device/apps/AppsFragment.java
@@ -16,8 +16,6 @@
 
 package com.android.tv.settings.device.apps;
 
-import android.content.Context;
-import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.os.Bundle;
 import android.os.Handler;
@@ -25,9 +23,7 @@
 import android.support.annotation.NonNull;
 import android.support.v17.preference.LeanbackPreferenceFragment;
 import android.support.v7.preference.Preference;
-import android.support.v7.preference.PreferenceCategory;
 import android.support.v7.preference.PreferenceGroup;
-import android.support.v7.preference.PreferenceScreen;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -135,37 +131,19 @@
 
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
-        final Context context = getPreferenceManager().getContext();
-
-        final PreferenceScreen screen =
-                getPreferenceManager().createPreferenceScreen(context);
-        screen.setTitle(R.string.device_apps);
         // TODO: show volume name somewhere?
 
+        setPreferencesFromResource(R.xml.apps, null);
+
         final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID);
 
-        if (TextUtils.isEmpty(volumeUuid)) {
-            final Preference permissionsPreference = new Preference(context);
-            permissionsPreference.setKey("Permissions");
-            permissionsPreference.setTitle(R.string.device_apps_permissions);
-            permissionsPreference.setIntent(new Intent(Intent.ACTION_MANAGE_PERMISSIONS));
-            screen.addPreference(permissionsPreference);
-        }
-        mDownloadedPreferenceGroup = new PreferenceCategory(context);
-        mDownloadedPreferenceGroup.setKey("DownloadedPreferenceGroup");
-        mDownloadedPreferenceGroup.setTitle(R.string.apps_downloaded);
-        screen.addPreference(mDownloadedPreferenceGroup);
-        mDownloadedPreferenceGroup.setOrderingAsAdded(false);
+        final Preference permissionPreference = findPreference("Permissions");
+        permissionPreference.setVisible(TextUtils.isEmpty(volumeUuid));
 
-        if (TextUtils.isEmpty(volumeUuid)) {
-            mSystemPreferenceGroup = new PreferenceCategory(context);
-            mSystemPreferenceGroup.setKey("SystemPreferenceGroup");
-            mSystemPreferenceGroup.setTitle(R.string.apps_system);
-            screen.addPreference(mSystemPreferenceGroup);
-            mSystemPreferenceGroup.setOrderingAsAdded(false);
-        }
+        mDownloadedPreferenceGroup = (PreferenceGroup) findPreference("DownloadedPreferenceGroup");
 
-        setPreferenceScreen(screen);
+        mSystemPreferenceGroup = (PreferenceGroup) findPreference("SystemPreferenceGroup");
+        mSystemPreferenceGroup.setVisible(TextUtils.isEmpty(volumeUuid));
     }
 
     @Override
diff --git a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppOp.java b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppOp.java
new file mode 100644
index 0000000..cb98004
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppOp.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2017 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.tv.settings.device.apps.specialaccess;
+
+import android.app.ActivityThread;
+import android.app.AppOpsManager;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settingslib.applications.ApplicationsState;
+
+/**
+ * Base class for managing app ops
+ */
+public abstract class ManageAppOp extends ManageApplications {
+    private static final String TAG = "ManageAppOps";
+
+    private IPackageManager mIPackageManager;
+    private AppOpsManager mAppOpsManager;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        mIPackageManager = ActivityThread.getPackageManager();
+        mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+        super.onCreate(savedInstanceState);
+    }
+
+    @NonNull
+    @Override
+    public ApplicationsState.AppFilter getFilter() {
+        return new ApplicationsState.AppFilter() {
+            @Override
+            public void init() {
+            }
+
+            @Override
+            public boolean filterApp(ApplicationsState.AppEntry entry) {
+                entry.extraInfo = createPermissionStateFor(entry.info.packageName, entry.info.uid);
+                return !shouldIgnorePackage(entry.info.packageName)
+                        && ((PermissionState) entry.extraInfo).isPermissible();
+            }
+        };
+    }
+
+    /**
+     * @return AppOps code
+     */
+    public abstract int getAppOpsOpCode();
+
+    /**
+     * @return Manifest permission string
+     */
+    public abstract String getPermission();
+
+    private boolean hasRequestedAppOpPermission(String permission, String packageName) {
+        try {
+            String[] packages = mIPackageManager.getAppOpPermissionPackages(permission);
+            return ArrayUtils.contains(packages, packageName);
+        } catch (RemoteException exc) {
+            Log.e(TAG, "PackageManager dead. Cannot get permission info");
+            return false;
+        }
+    }
+
+    private boolean hasPermission(int uid) {
+        try {
+            int result = mIPackageManager.checkUidPermission(getPermission(), uid);
+            return result == PackageManager.PERMISSION_GRANTED;
+        } catch (RemoteException e) {
+            Log.e(TAG, "PackageManager dead. Cannot get permission info");
+            return false;
+        }
+    }
+
+    private int getAppOpMode(int uid, String packageName) {
+        return mAppOpsManager.checkOpNoThrow(getAppOpsOpCode(), uid, packageName);
+    }
+
+    private PermissionState createPermissionStateFor(String packageName, int uid) {
+        return new PermissionState(
+                hasRequestedAppOpPermission(getPermission(), packageName),
+                hasPermission(uid),
+                getAppOpMode(uid, packageName));
+    }
+
+    /*
+     * Checks for packages that should be ignored for further processing
+     */
+    private boolean shouldIgnorePackage(String packageName) {
+        return packageName.equals("android") || packageName.equals(getContext().getPackageName());
+    }
+
+    /**
+     * Collection of information to be used as {@link ApplicationsState.AppEntry#extraInfo} objects
+     */
+    protected static class PermissionState {
+        public final boolean permissionRequested;
+        public final boolean permissionGranted;
+        public final int appOpMode;
+
+        private PermissionState(boolean permissionRequested, boolean permissionGranted,
+                int appOpMode) {
+            this.permissionRequested = permissionRequested;
+            this.permissionGranted = permissionGranted;
+            this.appOpMode = appOpMode;
+        }
+
+        /**
+         * @return True if the permission is granted
+         */
+        public boolean isAllowed() {
+            if (appOpMode == AppOpsManager.MODE_DEFAULT) {
+                return permissionGranted;
+            } else {
+                return appOpMode == AppOpsManager.MODE_ALLOWED;
+            }
+        }
+
+        /**
+         * @return True if the permission is relevant
+         */
+        public boolean isPermissible() {
+            return appOpMode != AppOpsManager.MODE_DEFAULT || permissionRequested;
+        }
+
+        @Override
+        public String toString() {
+            return "[permissionGranted: " + permissionGranted
+                    + ", permissionRequested: " + permissionRequested
+                    + ", appOpMode: " + appOpMode
+                    + "]";
+        }
+    }
+}
diff --git a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppUsageAccess.java b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppUsageAccess.java
new file mode 100644
index 0000000..ae4da3b
--- /dev/null
+++ b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageAppUsageAccess.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 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.tv.settings.device.apps.specialaccess;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.os.Bundle;
+import android.support.annotation.Keep;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.TwoStatePreference;
+
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.tv.settings.R;
+
+import java.util.Comparator;
+
+/**
+ * Fragment for controlling if apps can monitor app usage
+ */
+@Keep
+public class ManageAppUsageAccess extends ManageAppOp {
+
+    private AppOpsManager mAppOpsManager;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+    }
+
+    @Override
+    public int getAppOpsOpCode() {
+        return AppOpsManager.OP_GET_USAGE_STATS;
+    }
+
+    @Override
+    public String getPermission() {
+        return Manifest.permission.PACKAGE_USAGE_STATS;
+    }
+
+    @Nullable
+    @Override
+    public Comparator<ApplicationsState.AppEntry> getComparator() {
+        return ApplicationsState.ALPHA_COMPARATOR;
+    }
+
+    @NonNull
+    @Override
+    public Preference bindPreference(@NonNull Preference preference,
+            ApplicationsState.AppEntry entry) {
+        final TwoStatePreference switchPref = (SwitchPreference) preference;
+        switchPref.setTitle(entry.label);
+        switchPref.setKey(entry.info.packageName);
+        mApplicationsState.ensureIcon(entry);
+        switchPref.setIcon(entry.icon);
+        switchPref.setOnPreferenceChangeListener((pref, newValue) -> {
+            setAppUsageAccess(entry, (Boolean) newValue);
+            return true;
+        });
+
+        switchPref.setSummary(getPreferenceSummary(entry));
+        switchPref.setChecked(((PermissionState) entry.extraInfo).isAllowed());
+
+        return switchPref;
+    }
+
+    @NonNull
+    @Override
+    public Preference createAppPreference() {
+        return new SwitchPreference(getPreferenceManager().getContext());
+    }
+
+    @Override
+    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+        setPreferencesFromResource(R.xml.app_usage_access, null);
+    }
+
+    private CharSequence getPreferenceSummary(ApplicationsState.AppEntry entry) {
+        if (entry.extraInfo instanceof PermissionState) {
+            return getContext().getText(((PermissionState) entry.extraInfo).isPermissible()
+                    ? R.string.app_permission_summary_allowed
+                    : R.string.app_permission_summary_not_allowed);
+        } else {
+            return null;
+        }
+    }
+
+    private void setAppUsageAccess(ApplicationsState.AppEntry entry, boolean grant) {
+        mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS,
+                entry.info.uid, entry.info.packageName,
+                grant ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+        updateAppList();
+    }
+}
diff --git a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java
index 049a7ea..6709328 100644
--- a/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java
+++ b/Settings/src/com/android/tv/settings/device/apps/specialaccess/ManageExternalSources.java
@@ -17,12 +17,8 @@
 package com.android.tv.settings.device.apps.specialaccess;
 
 import android.Manifest;
-import android.app.ActivityThread;
 import android.app.AppOpsManager;
-import android.content.pm.IPackageManager;
-import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.support.annotation.Keep;
@@ -31,9 +27,7 @@
 import android.support.v14.preference.SwitchPreference;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.TwoStatePreference;
-import android.util.Log;
 
-import com.android.internal.util.ArrayUtils;
 import com.android.settingslib.applications.ApplicationsState;
 import com.android.tv.settings.R;
 
@@ -43,37 +37,23 @@
  * Fragment for controlling if apps can install other apps
  */
 @Keep
-public class ManageExternalSources extends ManageApplications {
-    private static final String TAG = "ManageExternalSources";
-
-    private IPackageManager mIpm;
+public class ManageExternalSources extends ManageAppOp {
     private AppOpsManager mAppOpsManager;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        mIpm = ActivityThread.getPackageManager();
         mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
         super.onCreate(savedInstanceState);
     }
 
-    @NonNull
     @Override
-    public ApplicationsState.AppFilter getFilter() {
-        return new ApplicationsState.AppFilter() {
+    public int getAppOpsOpCode() {
+        return AppOpsManager.OP_REQUEST_INSTALL_PACKAGES;
+    }
 
-            @Override
-            public void init() {
-            }
-
-            @Override
-            public boolean filterApp(ApplicationsState.AppEntry entry) {
-                if (!(entry.extraInfo instanceof InstallAppsState)) {
-                    loadInstallAppsState(entry);
-                }
-                InstallAppsState state = (InstallAppsState) entry.extraInfo;
-                return state.isPotentialAppSource();
-            }
-        };
+    @Override
+    public String getPermission() {
+        return Manifest.permission.REQUEST_INSTALL_PACKAGES;
     }
 
     @Nullable
@@ -107,9 +87,8 @@
             return true;
         });
 
-        loadInstallAppsState(entry);
-        InstallAppsState state = (InstallAppsState) entry.extraInfo;
-        switchPref.setChecked(state.canInstallApps());
+        PermissionState state = (PermissionState) entry.extraInfo;
+        switchPref.setChecked(state.isAllowed());
         switchPref.setSummary(getPreferenceSummary(entry));
         switchPref.setEnabled(canChange(entry));
         return switchPref;
@@ -143,14 +122,8 @@
                 return getContext().getString(R.string.disabled);
         }
 
-        final InstallAppsState appsState;
-        if (entry.extraInfo instanceof InstallAppsState) {
-            appsState = (InstallAppsState) entry.extraInfo;
-        } else {
-            entry.extraInfo = appsState =
-                    createInstallAppsStateFor(entry.info.packageName, entry.info.uid);
-        }
-        return getContext().getString(appsState.canInstallApps() ? R.string.external_source_trusted
+        return getContext().getString(((PermissionState) entry.extraInfo).isAllowed()
+                ? R.string.external_source_trusted
                 : R.string.external_source_untrusted);
     }
 
@@ -160,77 +133,4 @@
                 newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED);
         updateAppList();
     }
-
-
-    private boolean hasRequestedAppOpPermission(String permission, String packageName) {
-        try {
-            String[] packages = mIpm.getAppOpPermissionPackages(permission);
-            return ArrayUtils.contains(packages, packageName);
-        } catch (RemoteException exc) {
-            Log.e(TAG, "PackageManager dead. Cannot get permission info");
-            return false;
-        }
-    }
-
-    private boolean hasPermission(String permission, int uid) {
-        try {
-            int result = mIpm.checkUidPermission(permission, uid);
-            return result == PackageManager.PERMISSION_GRANTED;
-        } catch (RemoteException e) {
-            Log.e(TAG, "PackageManager dead. Cannot get permission info");
-            return false;
-        }
-    }
-
-    private int getAppOpMode(int appOpCode, int uid, String packageName) {
-        return mAppOpsManager.checkOpNoThrow(appOpCode, uid, packageName);
-    }
-
-    private void loadInstallAppsState(ApplicationsState.AppEntry entry) {
-        entry.extraInfo = createInstallAppsStateFor(entry.info.packageName, entry.info.uid);
-    }
-
-    private InstallAppsState createInstallAppsStateFor(String packageName, int uid) {
-        final InstallAppsState appState = new InstallAppsState();
-        appState.permissionRequested = hasRequestedAppOpPermission(
-                Manifest.permission.REQUEST_INSTALL_PACKAGES, packageName);
-        appState.permissionGranted = hasPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES,
-                uid);
-        appState.appOpMode = getAppOpMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, uid,
-                packageName);
-        return appState;
-    }
-
-    /**
-     * Collection of information to be used as {@link ApplicationsState.AppEntry#extraInfo} objects
-     */
-    private static class InstallAppsState {
-        public boolean permissionRequested;
-        public boolean permissionGranted;
-        public int appOpMode;
-
-        private InstallAppsState() {
-            this.appOpMode = AppOpsManager.MODE_DEFAULT;
-        }
-
-        public boolean canInstallApps() {
-            if (appOpMode == AppOpsManager.MODE_DEFAULT) {
-                return permissionGranted;
-            } else {
-                return appOpMode == AppOpsManager.MODE_ALLOWED;
-            }
-        }
-
-        public boolean isPotentialAppSource() {
-            return appOpMode != AppOpsManager.MODE_DEFAULT || permissionRequested;
-        }
-
-        @Override
-        public String toString() {
-            return "[permissionGranted: " + permissionGranted
-                    + ", permissionRequested: " + permissionRequested
-                    + ", appOpMode: " + appOpMode
-                    + "]";
-        }
-    }
 }