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/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);
+    }
+}