Add AutoRevoke UI

Add a PermissionController UI for the AutoRevoke feature
Test: none
Bug: 151252163

Change-Id: I98247633a23321b3a21b97c10f07512c89e72be5
diff --git a/PermissionController/res/layout/app_permission.xml b/PermissionController/res/layout/app_permission.xml
index 4831a8c..bde26eb 100644
--- a/PermissionController/res/layout/app_permission.xml
+++ b/PermissionController/res/layout/app_permission.xml
@@ -36,20 +36,20 @@
             <include layout="@layout/header_large" />
 
             <View
-                style="@style/LargeHeaderDivider"/>
+                style="@style/LargeHeaderDivider" />
 
             <LinearLayout
                 style="@style/AppPermissionSelection">
 
                 <TextView
                     android:id="@+id/permission_message"
-                    style="@style/AppPermissionMessage"/>
+                    style="@style/AppPermissionMessage" />
 
                 <RadioGroup
                     android:id="@+id/radiogroup"
                     android:animateLayoutChanges="true"
                     android:layout_width="match_parent"
-                    android:layout_height="wrap_content" >
+                    android:layout_height="wrap_content">
 
                     <RadioButton
                         android:id="@+id/allow_radio_button"
@@ -108,7 +108,7 @@
 
                     <LinearLayout
                         android:id="@+id/widget_frame"
-                        style="@style/AppPermissionWidgetFrame"/>
+                        style="@style/AppPermissionWidgetFrame" />
 
                 </LinearLayout>
 
diff --git a/PermissionController/res/navigation/nav_graph.xml b/PermissionController/res/navigation/nav_graph.xml
index 0934550..235b5f1 100644
--- a/PermissionController/res/navigation/nav_graph.xml
+++ b/PermissionController/res/navigation/nav_graph.xml
@@ -101,7 +101,6 @@
             app:popEnterAnim="@anim/activity_open_enter"/>
     </fragment>
 
-
     <fragment
         android:id="@+id/all_app_permissions"
         android:name="com.android.permissioncontroller.permission.ui.handheld.AllAppPermissionsFragment"
@@ -132,7 +131,5 @@
             app:enterAnim="@anim/activity_open_enter"
             app:popExitAnim="@anim/activity_close_exit"
             app:popEnterAnim="@anim/activity_open_enter"/>
-
     </fragment>
-
 </navigation>
\ No newline at end of file
diff --git a/PermissionController/res/values/strings.xml b/PermissionController/res/values/strings.xml
index 6ec493c..b378897 100644
--- a/PermissionController/res/values/strings.xml
+++ b/PermissionController/res/values/strings.xml
@@ -307,6 +307,24 @@
     <!-- Text for linking to the page that shows the apps with a given permission [CHAR LIMIT=none] -->
     <string name="app_permission_footer_permission_apps_link">See all apps with this permission</string>
 
+    <!-- Label for the auto revoke switch [CHAR LIMIT=60] -->
+    <string name="auto_revoke_label">Auto revoke permissions</string>
+
+    <!-- Summary for stating that auto revoke is disabled for this app[CHAR LIMIT=none] -->
+    <string name="auto_revoke_summary">To protect your data, permissions for this app will be removed if the app isn\u2019t used for a few months.</string>
+
+    <!-- Summary for stating that no permission groups that are granted and auto revocable[CHAR LIMIT=none] -->
+    <string name="auto_revocable_permissions_none">No auto revocable permissions are currently granted</string>
+
+    <!-- Summary for stating that one permission will be auto revoked[CHAR LIMIT=none] -->
+    <string name="auto_revocable_permissions_one"><xliff:g id="perm" example="location">%1$s</xliff:g> permission will be removed.</string>
+
+    <!-- Summary for stating that two permissions will be auto revoked[CHAR LIMIT=none] -->
+    <string name="auto_revocable_permissions_two"><xliff:g id="perm" example="location">%1$s</xliff:g> and <xliff:g id="perm" example="contacts">%2$s</xliff:g> permissions will be removed.</string>
+
+    <!-- Summary for stating that more than two permissions will be auto revoked[CHAR LIMIT=none] -->
+    <string name="auto_revocable_permissions_many">Permissions that will be removed: <xliff:g id="perms" example="location, contacts, and phone">%1$s</xliff:g>.</string>
+
     <!-- Label for showing a permission group's description in the header of the list of apps that have that permission [CHAR LIMIT=none] -->
     <string name="permission_description_summary_generic">Apps with this permission can <xliff:g id="description" example="record audio">%1$s</xliff:g></string>
 
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/AutoRevokeStateLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/AutoRevokeStateLiveData.kt
new file mode 100644
index 0000000..f9d5363
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/data/AutoRevokeStateLiveData.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2020 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.permissioncontroller.permission.data
+
+import android.app.AppOpsManager
+import android.app.AppOpsManager.MODE_ALLOWED
+import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
+import android.app.Application
+import android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT
+import android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import androidx.lifecycle.Observer
+import com.android.permissioncontroller.PermissionControllerApplication
+import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS
+import com.android.permissioncontroller.permission.model.livedatatypes.AutoRevokeState
+import com.android.permissioncontroller.permission.utils.KotlinUtils
+import com.android.permissioncontroller.permission.utils.Utils
+import kotlinx.coroutines.Job
+import java.util.concurrent.TimeUnit
+
+/**
+ * A LiveData which tracks the AutoRevoke state for one user package.
+ *
+ * @param app The current application
+ * @param packageName The package name whose state we want
+ * @param user The user for whom we want the package
+ */
+class AutoRevokeStateLiveData private constructor(
+    app: Application,
+    private val packageName: String,
+    private val user: UserHandle
+) : SmartAsyncMediatorLiveData<AutoRevokeState>(), AppOpsManager.OnOpChangedListener {
+
+    private val packagePermsLiveData =
+        PackagePermissionsLiveData[packageName, user]
+    private val packageLiveData = LightPackageInfoLiveData[packageName, user]
+    private val permStateLiveDatas = mutableMapOf<String, PermStateLiveData>()
+    private val appOpsManager = app.getSystemService(AppOpsManager::class.java)!!
+
+    init {
+        addSource(packagePermsLiveData) {
+            updateIfActive()
+        }
+        addSource(packageLiveData) {
+            updateIfActive()
+        }
+    }
+
+    override suspend fun loadDataAndPostValue(job: Job) {
+        val uid = packageLiveData.value?.uid
+        if (uid == null && packageLiveData.isInitialized) {
+            postValue(null)
+            return
+        } else if (uid == null) {
+            return
+        }
+
+        val groups = packagePermsLiveData.value?.keys?.filter { it != NON_RUNTIME_NORMAL_PERMS }
+        if (groups == null && packagePermsLiveData.isInitialized) {
+            postValue(null)
+            return
+        } else if (groups == null) {
+            return
+        }
+
+        addAndRemovePermStateLiveDatas(groups)
+
+        if (!permStateLiveDatas.all { it.value.isInitialized }) {
+            return
+        }
+
+        val revocable = appOpsManager.unsafeCheckOpNoThrow(
+            OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, uid, packageName) == MODE_ALLOWED
+        val autoRevokeState = mutableListOf<String>()
+        permStateLiveDatas.forEach { (groupName, liveData) ->
+            val default = liveData.value?.any { (_, permState) ->
+                permState.permFlags and (FLAG_PERMISSION_GRANTED_BY_DEFAULT or
+                    FLAG_PERMISSION_GRANTED_BY_ROLE) != 0
+            } ?: false
+            if (!default) {
+                autoRevokeState.add(groupName)
+            }
+        }
+
+        postValue(AutoRevokeState(isAutoRevokeEnabledGlobal(), revocable, autoRevokeState))
+    }
+
+    private fun isAutoRevokeEnabledGlobal(): Boolean {
+        val unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS,
+                Utils.PROPERTY_AUTO_REVOKE_UNUSED_THRESHOLD_MILLIS, TimeUnit.DAYS.toMillis(90))
+        val checkFrequency = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS,
+            Utils.PROPERTY_AUTO_REVOKE_CHECK_FREQUENCY_MILLIS, TimeUnit.DAYS.toMillis(1))
+        return unusedThreshold > 0 && checkFrequency > 0
+    }
+
+    private fun addAndRemovePermStateLiveDatas(groupNames: List<String>) {
+        val (toAdd, toRemove) = KotlinUtils.getMapAndListDifferences(groupNames,
+            permStateLiveDatas)
+
+        for (groupToAdd in toAdd) {
+            val permStateLiveData =
+                PermStateLiveData[packageName, groupToAdd, user]
+            permStateLiveDatas[groupToAdd] = permStateLiveData
+        }
+
+        for (groupToAdd in toAdd) {
+            postAddSource(permStateLiveDatas[groupToAdd]!!, Observer {
+                updateIfActive()
+            })
+        }
+
+        for (groupToRemove in toRemove) {
+            postRemoveSource(permStateLiveDatas[groupToRemove]!!)
+            permStateLiveDatas.remove(groupToRemove)
+        }
+    }
+
+    override fun onOpChanged(op: String?, packageName: String?) {
+        if (op == OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED && packageName == packageName) {
+            updateIfActive()
+        }
+    }
+
+    override fun onActive() {
+        super.onActive()
+        appOpsManager.startWatchingMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, packageName, this)
+        updateIfActive()
+    }
+
+    override fun onInactive() {
+        super.onInactive()
+        appOpsManager.stopWatchingMode(this)
+    }
+    /**
+     * Repository for AutoRevokeStateLiveDatas.
+     * <p> Key value is a pair of string package name and UserHandle, value is its corresponding
+     * LiveData.
+     */
+    companion object : DataRepository<Pair<String, UserHandle>, AutoRevokeStateLiveData>() {
+        override fun newValue(key: Pair<String, UserHandle>): AutoRevokeStateLiveData {
+            return AutoRevokeStateLiveData(PermissionControllerApplication.get(),
+                key.first, key.second)
+        }
+    }
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt
index c278308..43ec4a9 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt
@@ -29,6 +29,9 @@
 import com.android.permissioncontroller.permission.utils.ensureMainThread
 import com.android.permissioncontroller.permission.utils.getInitializedValue
 import com.android.permissioncontroller.permission.utils.shortStackTrace
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
 
 /**
  * A MediatorLiveData which tracks how long it has been inactive, compares new values before setting
@@ -161,6 +164,14 @@
         super.removeSource(toRemote)
     }
 
+    fun <S : Any?> postAddSource(source: LiveData<S>, onChanged: Observer<in S>) {
+        GlobalScope.launch(Main) { addSource(source, onChanged) }
+    }
+
+    fun <S : Any?> postRemoveSource(toRemote: LiveData<S>) {
+        GlobalScope.launch(Main) { removeSource(toRemote) }
+    }
+
     private fun <S : Any?> removeChild(liveData: LiveData<S>) {
         children.removeIf { it.first == liveData }
     }
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AppPermGroupUiInfo.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AppPermGroupUiInfo.kt
index 9f4cfe7..bebb108 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AppPermGroupUiInfo.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AppPermGroupUiInfo.kt
@@ -29,7 +29,7 @@
     val permGrantState: PermGrantState,
     val isSystem: Boolean
 ) {
-    enum class PermGrantState(val grantState: Int) {
+    enum class PermGrantState(private val grantState: Int) {
         PERMS_DENIED(0),
         PERMS_ALLOWED(1),
         PERMS_ALLOWED_FOREGROUND_ONLY(2),
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AutoRevokeState.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AutoRevokeState.kt
new file mode 100644
index 0000000..b0422f7
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/AutoRevokeState.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 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.permissioncontroller.permission.model.livedatatypes
+
+/**
+ * Tracks the state of auto revoke for a package
+ *
+ * @param isEnabledGlobal Whether or not the Auto Revoke feature is enabled globally
+ * @param isEnabledForApp Whether or not the OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED is set to
+ * MODE_ALLOWED for this package
+ * @param revocableGroupNames A list of which permission groups of this package are eligible for
+ * auto-revoke. A permission group is auto-revocable if it does not contain a default granted
+ * permission.
+ */
+class AutoRevokeState(
+    val isEnabledGlobal: Boolean,
+    val isEnabledForApp: Boolean,
+    val revocableGroupNames: List<String>
+) {
+
+    /**
+     * If the auto revoke switch should be shown.
+     */
+    val shouldShowSwitch = revocableGroupNames.isNotEmpty()
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java
index 255d27a..edb6078 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java
@@ -154,7 +154,6 @@
         return arguments;
     }
 
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -337,7 +336,6 @@
             setResult(DENIED_DO_NOT_ASK_AGAIN);
         });
 
-        // TODO(ntmyren): pass button states in a non-order specific way
         setButtonState(mAllowButton, states.get(ButtonType.ALLOW));
         setButtonState(mAllowAlwaysButton, states.get(ButtonType.ALLOW_ALWAYS));
         setButtonState(mAllowForegroundButton, states.get(ButtonType.ALLOW_FOREGROUND));
@@ -361,8 +359,6 @@
         }
     }
 
-
-
     private void setResult(@GrantPermissionsViewHandler.Result int result) {
         Intent intent = new Intent()
                 .putExtra(EXTRA_RESULT_PERMISSION_INTERACTED, mPermGroupName)
@@ -428,7 +424,7 @@
      *  1. `showDefaultDenyDialog`
      *  1. [DefaultDenyDialog.onCreateDialog]
      *  1. [AppPermissionViewModel.onDenyAnyWay]
-     * TODO: Remove once data can be passed between dialogs and fragments with nav component
+     * TODO ntmyren: Remove once data can be passed between dialogs and fragments with nav component
      *
      * @param changeRequest Whether background or foreground should be changed
      * @param messageId The Id of the string message to show
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
index e81d7c6..2711fb1 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
+import android.icu.text.ListFormatter;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -47,23 +48,26 @@
 import androidx.lifecycle.ViewModelProvider;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
 
 import com.android.permissioncontroller.PermissionControllerStatsLog;
 import com.android.permissioncontroller.R;
+import com.android.permissioncontroller.permission.model.livedatatypes.AutoRevokeState;
 import com.android.permissioncontroller.permission.ui.Category;
 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel;
+import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.GroupUiInfo;
 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory;
 import com.android.permissioncontroller.permission.utils.KotlinUtils;
 import com.android.permissioncontroller.permission.utils.Utils;
 import com.android.settingslib.HelpUtils;
 
 import java.text.Collator;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
 
-import kotlin.Triple;
-
 /**
  * Show and manage permission groups for an app.
  *
@@ -71,8 +75,12 @@
  */
 public final class AppPermissionGroupsFragment extends SettingsWithLargeHeader {
 
-    private static final String LOG_TAG = "ManagePermsFragment";
+    private static final String LOG_TAG = AppPermissionGroupsFragment.class.getSimpleName();
     private static final String IS_SYSTEM_PERMS_SCREEN = "_is_system_screen";
+    private static final String AUTO_REVOKE_CATEGORY_KEY = "_AUTO_REVOKE_KEY";
+    private static final String AUTO_REVOKE_SWITCH_KEY = "_AUTO_REVOKE_SWITCH_KEY";
+    private static final String AUTO_REVOKE_SUMMARY_KEY = "_AUTO_REVOKE_SUMMARY_KEY";
+    private static final String AUTO_REVOKE_PERMS_KEY = "_AUTO_REVOKE_PERMS_KEY";
 
     static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton";
 
@@ -139,10 +147,11 @@
         mIsSystemPermsScreen = getArguments().getBoolean(IS_SYSTEM_PERMS_SCREEN, true);
 
         AppPermissionGroupsViewModelFactory factory = new AppPermissionGroupsViewModelFactory(
-                getActivity().getApplication(), mPackageName, mUser);
+                mPackageName, mUser);
 
         mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class);
         mViewModel.getPackagePermGroupsLiveData().observe(this, this::updatePreferences);
+        mViewModel.getAutoRevokeLiveData().observe(this, this::setAutoRevokeToggleState);
 
         mCollator = Collator.getInstance(
                 getContext().getResources().getConfiguration().getLocales().get(0));
@@ -202,12 +211,17 @@
 
     }
 
-    private void updatePreferences(Map<Category, List<Triple<String, Boolean, Boolean>>> groupMap) {
+    private void createPreferenceScreenIfNeeded() {
         if (getPreferenceScreen() == null) {
             addPreferencesFromResource(R.xml.allowed_denied);
+            addAutoRevokePreferences(getPreferenceScreen());
             logAppPermissionsFragmentView();
             bindUi(this, mPackageName, mUser);
         }
+    }
+
+    private void updatePreferences(Map<Category, List<GroupUiInfo>> groupMap) {
+        createPreferenceScreenIfNeeded();
 
         Context context = getPreferenceManager().getContext();
         if (context == null) {
@@ -244,23 +258,21 @@
                 }
             }
 
-            for (Triple<String, Boolean, Boolean> groupTriple : groupMap.get(grantCategory)) {
-                String groupName = groupTriple.getFirst();
-                boolean isSystem = groupTriple.getSecond();
-                boolean isForegroundOnly = groupTriple.getThird();
+            for (GroupUiInfo uiInfo : groupMap.get(grantCategory)) {
+                String groupName = uiInfo.getGroupName();
 
                 PermissionControlPreference preference = new PermissionControlPreference(context,
                         mPackageName, groupName, mUser, AppPermissionGroupsFragment.class.getName(),
                         sessionId, grantCategory.getCategoryName(), true);
                 preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName));
                 preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName));
-                preference.setKey(preference.getTitle().toString());
-                if (isForegroundOnly) {
+                preference.setKey(groupName);
+                if (uiInfo.isForeground()) {
                     preference.setSummary(R.string.permission_subtitle_only_in_foreground);
                 }
-                if (isSystem == mIsSystemPermsScreen) {
+                if (uiInfo.isSystem() == mIsSystemPermsScreen) {
                     category.addPreference(preference);
-                } else if (!isSystem) {
+                } else if (!uiInfo.isSystem()) {
                     numExtraPerms++;
                 }
             }
@@ -281,6 +293,86 @@
             KotlinUtils.INSTANCE.sortPreferenceGroup(category, false,
                     this::comparePreferences);
         }
+
+        setAutoRevokeToggleState(mViewModel.getAutoRevokeLiveData().getValue());
+    }
+
+    private void addAutoRevokePreferences(PreferenceScreen screen) {
+        Context context = screen.getPreferenceManager().getContext();
+
+        PreferenceCategory autoRevokeCategory = new PreferenceCategory(context);
+        autoRevokeCategory.setKey(AUTO_REVOKE_CATEGORY_KEY);
+        screen.addPreference(autoRevokeCategory);
+
+        SwitchPreference autoRevokeSwitch = new SwitchPreference(context);
+        autoRevokeSwitch.setOnPreferenceClickListener((preference) -> {
+            mViewModel.setAutoRevoke(autoRevokeSwitch.isChecked());
+            return true;
+        });
+        autoRevokeSwitch.setTitle(R.string.auto_revoke_label);
+        autoRevokeSwitch.setKey(AUTO_REVOKE_SWITCH_KEY);
+        autoRevokeCategory.addPreference(autoRevokeSwitch);
+
+        Preference autoRevokeSummary = new Preference(context);
+        autoRevokeSummary.setIcon(Utils.applyTint(getActivity(), R.drawable.ic_info_outline,
+                android.R.attr.colorControlNormal));
+        autoRevokeSummary.setKey(AUTO_REVOKE_SUMMARY_KEY);
+        autoRevokeSummary.setSummary(R.string.auto_revoke_summary);
+        autoRevokeCategory.addPreference(autoRevokeSummary);
+
+        Preference autoRevokePerms = new Preference(context);
+        autoRevokePerms.setKey(AUTO_REVOKE_PERMS_KEY);
+        autoRevokeCategory.addPreference(autoRevokePerms);
+    }
+
+    private void setAutoRevokeToggleState(AutoRevokeState state) {
+        if (state == null || !mViewModel.getPackagePermGroupsLiveData().isInitialized()
+                || getListView() == null || getView() == null) {
+            return;
+        }
+
+        PreferenceCategory autoRevokeCategory = getPreferenceScreen()
+                .findPreference(AUTO_REVOKE_CATEGORY_KEY);
+        SwitchPreference autoRevokeSwitch = autoRevokeCategory.findPreference(
+                AUTO_REVOKE_SWITCH_KEY);
+        Preference autoRevokePerms = autoRevokeCategory.findPreference(AUTO_REVOKE_PERMS_KEY);
+        Preference autoRevokeSummary = autoRevokeCategory.findPreference(AUTO_REVOKE_SUMMARY_KEY);
+
+        if (!state.isEnabledGlobal() || !state.getShouldShowSwitch()) {
+            autoRevokeSwitch.setVisible(false);
+            autoRevokePerms.setVisible(false);
+            autoRevokeSummary.setVisible(false);
+            return;
+        }
+        autoRevokeSwitch.setVisible(true);
+        autoRevokePerms.setVisible(true);
+        autoRevokeSummary.setVisible(true);
+        autoRevokeSwitch.setChecked(state.isEnabledForApp());
+
+        List<String> groupLabels = new ArrayList<>();
+        for (String groupName : state.getRevocableGroupNames()) {
+            PreferenceCategory category = getPreferenceScreen().findPreference(
+                    Category.ALLOWED.getCategoryName());
+            Preference pref = category.findPreference(groupName);
+            if (pref != null) {
+                groupLabels.add(pref.getTitle().toString());
+            }
+        }
+
+        groupLabels.sort(mCollator);
+        if (groupLabels.isEmpty()) {
+            autoRevokePerms.setSummary(R.string.auto_revocable_permissions_none);
+        } else if (groupLabels.size() == 1) {
+            autoRevokePerms.setSummary(getString(R.string.auto_revocable_permissions_one,
+                    groupLabels.get(0)));
+        } else if (groupLabels.size() == 2) {
+            autoRevokePerms.setSummary(getString(R.string.auto_revocable_permissions_two,
+                    groupLabels.get(0), groupLabels.get(1)));
+
+        } else {
+            autoRevokePerms.setSummary(getString(R.string.auto_revocable_permissions_many,
+                    ListFormatter.getInstance().format(groupLabels)));
+        }
     }
 
     private int comparePreferences(Preference lhs, Preference rhs) {
@@ -347,7 +439,8 @@
                 category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND;
             }
 
-            logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), category);
+            logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(),
+                    category);
         }
 
         PreferenceCategory denied = findPreference(Category.DENIED.getCategoryName());
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SettingsWithLargeHeader.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SettingsWithLargeHeader.java
index e857fd9..cbd4a0c 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SettingsWithLargeHeader.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SettingsWithLargeHeader.java
@@ -41,7 +41,7 @@
  */
 public abstract class SettingsWithLargeHeader extends PermissionsFrameFragment  {
     static final String HEADER_KEY = " HEADER_PREFERENCE";
-    static final int HEADER_SORT_FIRST = -1;
+    private static final int HEADER_SORT_FIRST = -2;
 
     private View mHeader;
     private LargeHeaderPreference mHeaderPreference;
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SmartIconLoadPackagePermissionPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SmartIconLoadPackagePermissionPreference.kt
index 209bdba..07c6776 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SmartIconLoadPackagePermissionPreference.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/SmartIconLoadPackagePermissionPreference.kt
@@ -39,7 +39,7 @@
  * @param user The user whose package icon will be retrieved
  * @param context The current context
  */
-open class SmartIconLoadPackagePermissionPreference @JvmOverloads constructor(
+open class SmartIconLoadPackagePermissionPreference constructor(
     private val app: Application,
     private val packageName: String,
     private val user: UserHandle,
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
index 9d57b73..5043f01 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
@@ -16,15 +16,21 @@
 
 package com.android.permissioncontroller.permission.ui.model
 
-import android.app.Application
+import android.app.AppOpsManager
+import android.app.AppOpsManager.MODE_ALLOWED
+import android.app.AppOpsManager.MODE_IGNORED
+import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
 import android.os.Bundle
 import android.os.UserHandle
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import androidx.navigation.fragment.findNavController
+import com.android.permissioncontroller.PermissionControllerApplication
 import com.android.permissioncontroller.R
 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData
+import com.android.permissioncontroller.permission.data.AutoRevokeStateLiveData
+import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData
 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData
 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS
 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
@@ -33,34 +39,29 @@
 import com.android.permissioncontroller.permission.ui.Category
 import com.android.permissioncontroller.permission.utils.KotlinUtils
 import com.android.permissioncontroller.permission.utils.Utils
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
 
 /**
  * ViewModel for the AppPermissionGroupsFragment. Has a liveData with the UI information for all
  * permission groups that this package requests runtime permissions from
  *
- * @param app The current application
  * @param packageName The name of the package this viewModel is representing
  * @param user The user of the package this viewModel is representing
  */
 class AppPermissionGroupsViewModel(
-    app: Application,
     private val packageName: String,
     private val user: UserHandle
 ) : ViewModel() {
 
-    val packagePermGroupsLiveData = PackagePermGroupsLiveData(app, packageName, user)
-
     /**
      * LiveData whose data is a map of grant category (either allowed or denied) to a list
      * of permission group names that match the key, and two booleans representing if this is a
-     * system group, and, if it is allowed in the foreground only.
+     * system group, and a subtitle resource ID, if applicable.
      */
-    inner class PackagePermGroupsLiveData(
-        private val app: Application,
-        private val packageName: String,
-        private val user: UserHandle
-    ) : SmartUpdateMediatorLiveData<@JvmSuppressWildcards
-    Map<Category, List<Triple<String, Boolean, Boolean>>>>() {
+    val packagePermGroupsLiveData = object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards
+    Map<Category, List<GroupUiInfo>>>() {
 
         private val packagePermsLiveData =
             PackagePermissionsLiveData[packageName, user]
@@ -89,7 +90,7 @@
             }
 
             val groupGrantStates = mutableMapOf<Category,
-                MutableList<Triple<String, Boolean, Boolean>>>()
+                MutableList<GroupUiInfo>>()
             groupGrantStates[Category.ALLOWED] = mutableListOf()
             groupGrantStates[Category.ASK] = mutableListOf()
             groupGrantStates[Category.DENIED] = mutableListOf()
@@ -99,15 +100,16 @@
                 appPermGroupUiInfoLiveDatas[groupName]?.value?.let { uiInfo ->
                     when (uiInfo.permGrantState) {
                         PermGrantState.PERMS_ALLOWED -> groupGrantStates[Category.ALLOWED]!!.add(
-                                Triple(groupName, isSystem, false))
+                            GroupUiInfo(groupName, isSystem))
                         PermGrantState.PERMS_ALLOWED_ALWAYS -> groupGrantStates[
-                            Category.ALLOWED]!!.add(Triple(groupName, isSystem, false))
+                            Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem))
                         PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> groupGrantStates[
-                            Category.ALLOWED]!!.add(Triple(groupName, isSystem, true))
+                            Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem,
+                            isForeground = true))
                         PermGrantState.PERMS_DENIED -> groupGrantStates[Category.DENIED]!!.add(
-                                Triple(groupName, isSystem, false))
+                            GroupUiInfo(groupName, isSystem))
                         PermGrantState.PERMS_ASK -> groupGrantStates[Category.ASK]!!.add(
-                                Triple(groupName, isSystem, false))
+                            GroupUiInfo(groupName, isSystem))
                     }
                 }
             }
@@ -138,6 +140,28 @@
         }
     }
 
+    data class GroupUiInfo(
+        val groupName: String,
+        val isSystem: Boolean = false,
+        val isForeground: Boolean = false
+    )
+
+    val autoRevokeLiveData = AutoRevokeStateLiveData[packageName, user]
+
+    fun setAutoRevoke(enabled: Boolean) {
+        GlobalScope.launch(IO) {
+            val aom = PermissionControllerApplication.get()
+                .getSystemService(AppOpsManager::class.java)!!
+            val packageInfo = LightPackageInfoLiveData[packageName, user].getInitializedValue()
+            val mode = if (enabled) {
+                MODE_ALLOWED
+            } else {
+                MODE_IGNORED
+            }
+            aom.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, packageInfo.uid, mode)
+        }
+    }
+
     fun showExtraPerms(fragment: Fragment, args: Bundle) {
         fragment.findNavController().navigate(R.id.perm_groups_to_extra, args)
     }
@@ -150,18 +174,16 @@
 /**
  * Factory for an AppPermissionGroupsViewModel
  *
- * @param app The current application
  * @param packageName The name of the package this viewModel is representing
  * @param user The user of the package this viewModel is representing
  */
 class AppPermissionGroupsViewModelFactory(
-    private val app: Application,
     private val packageName: String,
     private val user: UserHandle
 ) : ViewModelProvider.Factory {
 
     override fun <T : ViewModel> create(modelClass: Class<T>): T {
         @Suppress("UNCHECKED_CAST")
-        return AppPermissionGroupsViewModel(app, packageName, user) as T
+        return AppPermissionGroupsViewModel(packageName, user) as T
     }
 }
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt
index aa9de50..9d06a90 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt
@@ -118,10 +118,6 @@
     private var lightAppPermGroup: LightAppPermGroup? = null
 
     /**
-     * A livedata which computes the state of the radio buttons
-     */
-    val buttonStateLiveData = AppPermButtonStateLiveData()
-    /**
      * A livedata which determines which detail string, if any, should be shown
      */
     val detailResIdLiveData = MutableLiveData<Pair<Int, Int?>>()
@@ -139,7 +135,10 @@
         constructor() : this(false, true, false, null)
     }
 
-    inner class AppPermButtonStateLiveData
+    /**
+     * A livedata which computes the state of the radio buttons
+     */
+    val buttonStateLiveData = object
         : SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<ButtonType, ButtonState>>() {
 
         private val appPermGroupLiveData = LightAppPermGroupLiveData[packageName, permGroupName,
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt
index fd6f7a8..d77e7e9 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/AndroidUtils.kt
@@ -26,7 +26,7 @@
 import android.os.UserHandle
 
 /**
- * Gets an [Application] instance form a regular [Context]
+ * Gets an [Application] instance from a regular [Context]
  */
 val Context.application: Application get() = when (this) {
     is Activity -> application
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt
index e7b346a..a1fac05 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt
@@ -257,7 +257,12 @@
      *
      * @return The package's icon, or null, if the package does not exist
      */
-    fun getBadgedPackageIcon(app: Application, packageName: String, user: UserHandle): Drawable? {
+    @JvmOverloads
+    fun getBadgedPackageIcon(
+        app: Application,
+        packageName: String,
+        user: UserHandle
+    ): Drawable? {
         return try {
             val userContext = Utils.getUserContext(app, user)
             val appInfo = userContext.packageManager.getApplicationInfo(packageName, 0)
@@ -444,6 +449,7 @@
         newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_USER_FIXED)
         newFlags = newFlags.setFlag(PackageManager.FLAG_PERMISSION_USER_SET)
         newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_ONE_TIME)
+        newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_AUTO_REVOKED)
 
         // If we newly grant background access to the fine location, double-guess the user some
         // time later if this was really the right choice.
@@ -453,9 +459,9 @@
                 val bgPerm = group.permissions[perm.backgroundPermission]
                 triggerLocationAccessCheck = bgPerm?.isGrantedIncludingAppOp == true
             } else if (perm.name == ACCESS_BACKGROUND_LOCATION) {
-                    val fgPerm = group.permissions[ACCESS_FINE_LOCATION]
-                    triggerLocationAccessCheck = fgPerm?.isGrantedIncludingAppOp == true
-                }
+                val fgPerm = group.permissions[ACCESS_FINE_LOCATION]
+                triggerLocationAccessCheck = fgPerm?.isGrantedIncludingAppOp == true
+            }
             if (triggerLocationAccessCheck) {
                 // trigger location access check
                 LocationAccessCheck(app, null).checkLocationAccessSoon()
@@ -544,7 +550,7 @@
             val isBackgroundPerm = permName in group.backgroundPermNames
             if (isBackgroundPerm == revokeBackground) {
                 val (newPerm, shouldKill) =
-                        revokeRuntimePermission(app, perm, userFixed, oneTime, group)
+                    revokeRuntimePermission(app, perm, userFixed, oneTime, group)
                 newPerms[newPerm.name] = newPerm
                 shouldKillForAnyPermission = shouldKillForAnyPermission || shouldKill
             }
@@ -616,10 +622,10 @@
         // Update the permission flags.
         // Take a note that the user fixed the permission, if applicable.
         newFlags = if (userFixed) newFlags.setFlag(PackageManager.FLAG_PERMISSION_USER_FIXED)
-            else newFlags.clearFlag(PackageManager.FLAG_PERMISSION_USER_FIXED)
+        else newFlags.clearFlag(PackageManager.FLAG_PERMISSION_USER_FIXED)
         newFlags = newFlags.setFlag(PackageManager.FLAG_PERMISSION_USER_SET)
         newFlags = if (oneTime) newFlags.setFlag(PackageManager.FLAG_PERMISSION_ONE_TIME)
-            else newFlags.clearFlag(PackageManager.FLAG_PERMISSION_ONE_TIME)
+        else newFlags.clearFlag(PackageManager.FLAG_PERMISSION_ONE_TIME)
 
         if (perm.flags != newFlags) {
             val flagMask = PackageManager.FLAG_PERMISSION_USER_SET or