Update settings for app hibernation

Created an preference in Apps & Notifcations page as the entry point for
apps that are hibernated. Also added a switch preference in AppInfo page
for users to exempt an app from hibernation.

Bug: 181172051
Test: HibernatedAppsPreferenceControllerTest;
HibernationSwitchPreferenceControllerTest;
AppInfoDashboardFragmentTest

Change-Id: I72a90ab391cd521150fc155a6d9c67c846b7360d
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7e10006..c464da0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9800,6 +9800,18 @@
     <!-- Runtime permissions preference summary, which describes what the permission manager does. [CHAR LIMIT=NONE] -->
     <string name="runtime_permissions_summary_control_app_access">Control app access to your data</string>
 
+    <!-- Label for showing apps that have not been used for months. [CHAR LIMIT=40]-->
+    <string name="unused_apps">Unused apps</string>
+
+    <!-- Summary of number of apps that have not been used for months. [CHAR LIMIT=40]-->
+    <plurals name="unused_apps_summary">
+        <item quantity="one"><xliff:g id="count" example="1">%d</xliff:g> unused app</item>
+        <item quantity="other"><xliff:g id="count" example="10">%d</xliff:g> unused apps</item>
+    </plurals>
+
+    <!-- Label of a switch preference that controls whether the system will remove the permissions and free up space when the app has not been used for months [CHAR LIMIT=40]-->
+    <string name="unused_apps_switch">Remove permissions and free up space</string>
+
     <!-- Label for showing all apps in list [CHAR LIMIT=30] -->
     <string name="filter_all_apps">All apps</string>
     <!-- Label for showing enabled apps in list [CHAR LIMIT=30] -->
diff --git a/res/xml/app_and_notification.xml b/res/xml/app_and_notification.xml
index 49d8e9b..8b4c912 100644
--- a/res/xml/app_and_notification.xml
+++ b/res/xml/app_and_notification.xml
@@ -83,6 +83,16 @@
         <intent android:action="android.intent.action.MANAGE_PERMISSIONS"/>
     </Preference>
 
+    <Preference
+        android:key="hibernated_apps"
+        android:title="@string/unused_apps"
+        android:summary="@string/summary_placeholder"
+        android:order="13"
+        settings:keywords="app_hibernation_key"
+        settings:controller="com.android.settings.applications.HibernatedAppsPreferenceController">
+        <intent android:action="android.intent.action.MANAGE_UNUSED_APPS"/>
+    </Preference>
+
     <com.android.settingslib.RestrictedPreference
         android:key="app_and_notif_cell_broadcast_settings"
         android:title="@string/cell_broadcast_settings"
diff --git a/res/xml/app_info_settings.xml b/res/xml/app_info_settings.xml
index a76f0d9..f5d927e 100644
--- a/res/xml/app_info_settings.xml
+++ b/res/xml/app_info_settings.xml
@@ -120,6 +120,19 @@
         android:title="@string/sms_application_title"
         android:summary="@string/summary_placeholder" />
 
+    <PreferenceCategory
+        android:key="app_hibernation_info"
+        android:title="@string/unused_apps"
+        settings:controller=
+            "com.android.settings.applications.appinfo.AppHibernationPreferenceCategoryController">
+
+        <SwitchPreference
+            android:key="hibernation_switch"
+            android:title="@string/unused_apps_switch"
+            settings:controller=
+                "com.android.settings.applications.appinfo.HibernationSwitchPreferenceController" />
+    </PreferenceCategory>
+
     <!-- Advanced apps settings -->
     <PreferenceCategory
         android:key="advanced_app_info"
diff --git a/res/xml/app_info_settings_v2.xml b/res/xml/app_info_settings_v2.xml
index 574fc01..80c449a 100644
--- a/res/xml/app_info_settings_v2.xml
+++ b/res/xml/app_info_settings_v2.xml
@@ -134,6 +134,19 @@
         android:title="@string/sms_application_title"
         android:summary="@string/summary_placeholder" />
 
+    <PreferenceCategory
+        android:key="app_hibernation_info"
+        android:title="@string/unused_apps"
+        settings:controller=
+            "com.android.settings.applications.appinfo.AppHibernationPreferenceCategoryController">
+
+        <SwitchPreference
+            android:key="hibernation_switch"
+            android:title="@string/unused_apps_switch"
+            settings:controller=
+                "com.android.settings.applications.appinfo.HibernationSwitchPreferenceController" />
+    </PreferenceCategory>
+
     <!-- Advanced apps settings -->
     <PreferenceCategory
         android:key="advanced_app_info"
diff --git a/res/xml/apps.xml b/res/xml/apps.xml
index df295bf..cefb67a 100644
--- a/res/xml/apps.xml
+++ b/res/xml/apps.xml
@@ -67,6 +67,16 @@
         android:order="10"/>
 
     <Preference
+        android:key="hibernated_apps"
+        android:title="@string/unused_apps"
+        android:summary="@string/summary_placeholder"
+        android:order="15"
+        settings:keywords="app_hibernation_key"
+        settings:controller="com.android.settings.applications.HibernatedAppsPreferenceController">
+        <intent android:action="android.intent.action.MANAGE_UNUSED_APPS"/>
+    </Preference>
+
+    <Preference
         android:key="special_access_v2"
         android:fragment="com.android.settings.applications.specialaccess.SpecialAccessSettings"
         android:title="@string/special_access"
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index e44ee78..4bf97cc 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -153,6 +153,9 @@
     public static final String PROPERTY_LOCATION_INDICATOR_SETTINGS_ENABLED =
             "location_indicator_settings_enabled";
 
+    /** Whether or not app hibernation is enabled on the device **/
+    public static final String PROPERTY_APP_HIBERNATION_ENABLED = "app_hibernation_enabled";
+
     /**
      * Finds a matching activity for a preference's intent. If a matching
      * activity is not found, it will remove the preference.
diff --git a/src/com/android/settings/applications/HibernatedAppsPreferenceController.java b/src/com/android/settings/applications/HibernatedAppsPreferenceController.java
new file mode 100644
index 0000000..40cbb2e
--- /dev/null
+++ b/src/com/android/settings/applications/HibernatedAppsPreferenceController.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.applications;
+
+import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
+
+import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+/**
+ * A preference controller handling the logic for updating summary of hibernated apps.
+ * TODO(b/181172051): add intent to launch Auto Revoke UI in app_and_notification.xml
+ */
+public final class HibernatedAppsPreferenceController extends BasePreferenceController {
+    private static final String TAG = "HibernatedAppsPrefController";
+
+    public HibernatedAppsPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return isHibernationEnabled() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        final int numHibernated = getNumHibernated();
+        return mContext.getResources().getQuantityString(
+                R.plurals.unused_apps_summary, numHibernated, numHibernated);
+    }
+
+    private int getNumHibernated() {
+        //TODO(b/181172051): hook into hibernation service to get the number of hibernated apps.
+        return 0;
+    }
+
+    private static boolean isHibernationEnabled() {
+        return DeviceConfig.getBoolean(
+                NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, false);
+    }
+}
diff --git a/src/com/android/settings/applications/appinfo/AppHibernationPreferenceCategoryController.java b/src/com/android/settings/applications/appinfo/AppHibernationPreferenceCategoryController.java
new file mode 100644
index 0000000..ef89168
--- /dev/null
+++ b/src/com/android/settings/applications/appinfo/AppHibernationPreferenceCategoryController.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 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.applications.appinfo;
+
+import android.content.Context;
+
+import com.android.settings.widget.PreferenceCategoryController;
+
+/**
+ * A preference category controller serves as the parent for app hibernation related preference.
+ */
+public final class AppHibernationPreferenceCategoryController extends PreferenceCategoryController {
+    public AppHibernationPreferenceCategoryController(Context context, String key) {
+        super(context, key);
+    }
+}
diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
index 1878f5f..6a5c5df 100755
--- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
+++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
@@ -170,6 +170,13 @@
             use(ExtraAppInfoPreferenceController.class).setPackageName(packageName);
         }
 
+        final HibernationSwitchPreferenceController appHibernationSettings =
+                use(HibernationSwitchPreferenceController.class);
+        appHibernationSettings.setParentFragment(this);
+        appHibernationSettings.setPackage(packageName);
+        use(AppHibernationPreferenceCategoryController.class).setChildren(
+                Arrays.asList(appHibernationSettings));
+
         final WriteSystemSettingsPreferenceController writeSystemSettings =
                 use(WriteSystemSettingsPreferenceController.class);
         writeSystemSettings.setParentFragment(this);
diff --git a/src/com/android/settings/applications/appinfo/HibernationSwitchPreferenceController.java b/src/com/android/settings/applications/appinfo/HibernationSwitchPreferenceController.java
new file mode 100644
index 0000000..8ab2c9d
--- /dev/null
+++ b/src/com/android/settings/applications/appinfo/HibernationSwitchPreferenceController.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 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.applications.appinfo;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED;
+import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
+
+import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * A PreferenceController handling the logic for exempting hibernation of app
+ */
+public final class HibernationSwitchPreferenceController extends AppInfoPreferenceControllerBase
+        implements LifecycleObserver, AppOpsManager.OnOpChangedListener,
+        Preference.OnPreferenceChangeListener {
+    private static final String TAG = "HibernationSwitchPrefController";
+    private String mPackageName;
+    private final AppOpsManager mAppOpsManager;
+    private int mPackageUid;
+    @VisibleForTesting
+    boolean mIsPackageSet;
+    private boolean mIsPackageExemptByDefault;
+
+    public HibernationSwitchPreferenceController(Context context,
+            String preferenceKey) {
+        super(context, preferenceKey);
+        mAppOpsManager = context.getSystemService(AppOpsManager.class);
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+    public void onResume() {
+        if (mIsPackageSet) {
+            mAppOpsManager.startWatchingMode(
+                    OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageName, this);
+        }
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+    public void onPause() {
+        mAppOpsManager.stopWatchingMode(this);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return isHibernationEnabled() && mIsPackageSet ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+    }
+
+    /**
+     * Set the package. And also retrieve details from package manager. Some packages may be
+     * exempted from hibernation by default.
+     * @param packageName The name of the package whose hibernation state to be managed.
+     */
+    void setPackage(@NonNull String packageName) {
+        mPackageName = packageName;
+        final PackageManager packageManager = mContext.getPackageManager();
+
+        // Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R
+        final int maxTargetSdkVersionForExemptApps =
+                packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+                        ? android.os.Build.VERSION_CODES.R
+                        : android.os.Build.VERSION_CODES.Q;
+        try {
+            mPackageUid = packageManager.getPackageUidAsUser(
+                    packageName, mContext.getUserId());
+            mIsPackageExemptByDefault = packageManager.getTargetSdkVersion(packageName)
+                    <= maxTargetSdkVersionForExemptApps;
+            mIsPackageSet = true;
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.w(TAG, "Package [" + mPackageName + "] is not found!");
+            mIsPackageSet = false;
+        }
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        super.updateState(preference);
+        ((SwitchPreference) preference).setChecked(!isPackageHibernationExemptByUser());
+    }
+
+    @VisibleForTesting
+    boolean isPackageHibernationExemptByUser() {
+        if (!mIsPackageSet) return true;
+        final int mode = mAppOpsManager.unsafeCheckOpNoThrow(
+                OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid, mPackageName);
+
+        return mode == MODE_DEFAULT ? mIsPackageExemptByDefault : mode != MODE_ALLOWED;
+    }
+
+    @Override
+    public void onOpChanged(String op, String packageName) {
+        if (OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED.equals(op)
+                && TextUtils.equals(mPackageName, packageName)) {
+            updateState(mPreference);
+        }
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object isChecked) {
+        try {
+            mAppOpsManager.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid,
+                    (boolean) isChecked ? MODE_ALLOWED : MODE_IGNORED);
+        } catch (RuntimeException e) {
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean isHibernationEnabled() {
+        return DeviceConfig.getBoolean(
+                NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, false);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java
new file mode 100644
index 0000000..cf4c53e
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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.applications;
+
+import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
+
+import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * TODO(b/181172051): test getNumberHibernated() when the API implemented
+ */
+@RunWith(AndroidJUnit4.class)
+public class HibernatedAppsPreferenceControllerTest {
+
+    private static final String KEY = "key";
+    private Context mContext;
+    private HibernatedAppsPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
+                "true", false);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        mController = new HibernatedAppsPreferenceController(mContext, KEY);
+    }
+
+    @Test
+    public void getAvailabilityStatus_featureDisabled_shouldNotReturnAvailable() {
+        DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
+                "false", true);
+
+        assertThat((mController).getAvailabilityStatus()).isNotEqualTo(AVAILABLE);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/applications/appinfo/HibernationSwitchPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/appinfo/HibernationSwitchPreferenceControllerTest.java
new file mode 100644
index 0000000..e27942a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/appinfo/HibernationSwitchPreferenceControllerTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 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.applications.appinfo;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED;
+import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
+
+import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.provider.DeviceConfig;
+
+import androidx.preference.SwitchPreference;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class HibernationSwitchPreferenceControllerTest {
+    private static final int PACKAGE_UID = 1;
+    private static final String INVALID_PACKAGE_NAME = "invalid_package";
+    private static final String KEY = "key";
+    private static final String VALID_PACKAGE_NAME = "package";
+    private static final String EXEMPTED_PACKAGE_NAME = "exempted_package";
+    private static final String UNEXEMPTED_PACKAGE_NAME = "unexempted_package";
+    @Mock
+    private AppOpsManager mAppOpsManager;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private SwitchPreference mPreference;
+
+    private HibernationSwitchPreferenceController mController;
+    private Context mContext;
+
+    @Before
+    public void setUp() throws PackageManager.NameNotFoundException {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
+        when(mPackageManager.getPackageUidAsUser(eq(VALID_PACKAGE_NAME), anyInt()))
+                .thenReturn(PACKAGE_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(INVALID_PACKAGE_NAME), anyInt()))
+                .thenThrow(new PackageManager.NameNotFoundException());
+        when(mPackageManager.getTargetSdkVersion(eq(EXEMPTED_PACKAGE_NAME)))
+                .thenReturn(android.os.Build.VERSION_CODES.Q);
+        when(mPackageManager.getTargetSdkVersion(eq(UNEXEMPTED_PACKAGE_NAME)))
+                .thenReturn(android.os.Build.VERSION_CODES.S);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
+                "true", true /* makeDefault */);
+        mController = new HibernationSwitchPreferenceController(mContext, KEY);
+        when(mPreference.getKey()).thenReturn(mController.getPreferenceKey());
+    }
+
+    @Test
+    public void getAvailabilityStatus_featureNotEnabled_shouldNotReturnAvailable() {
+        mController.setPackage(VALID_PACKAGE_NAME);
+        DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
+                "false", true /* makeDefault */);
+
+        assertThat(mController.getAvailabilityStatus()).isNotEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_invalidPackage_shouldReturnNotAvailable() {
+        mController.setPackage(INVALID_PACKAGE_NAME);
+
+        assertThat(mController.getAvailabilityStatus()).isNotEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_validPackage_shouldReturnAvailable() {
+        mController.setPackage(VALID_PACKAGE_NAME);
+
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void updateState_exemptedByDefaultPackage_shouldNotCheck() {
+        when(mAppOpsManager.unsafeCheckOpNoThrow(
+                eq(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED), anyInt(), eq(EXEMPTED_PACKAGE_NAME)))
+                .thenReturn(MODE_DEFAULT);
+        mController.setPackage(EXEMPTED_PACKAGE_NAME);
+
+        mController.updateState(mPreference);
+
+        verify(mPreference).setChecked(false);
+    }
+
+    @Test
+    public void updateState_exemptedPackageOverrideByUser_shouldCheck() {
+        when(mAppOpsManager.unsafeCheckOpNoThrow(
+                eq(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED), anyInt(), eq(EXEMPTED_PACKAGE_NAME)))
+                .thenReturn(MODE_ALLOWED);
+        mController.setPackage(EXEMPTED_PACKAGE_NAME);
+
+        mController.updateState(mPreference);
+
+        verify(mPreference).setChecked(true);
+    }
+
+    @Test
+    public void updateState_unexemptedPackageOverrideByUser_shouldNotCheck() {
+        when(mAppOpsManager.unsafeCheckOpNoThrow(
+                eq(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED), anyInt(), eq(UNEXEMPTED_PACKAGE_NAME)))
+                .thenReturn(MODE_IGNORED);
+        mController.setPackage(UNEXEMPTED_PACKAGE_NAME);
+
+        mController.updateState(mPreference);
+
+        verify(mPreference).setChecked(false);
+    }
+}