1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.applications.appinfo;
18 
19 import static android.app.AppOpsManager.MODE_ALLOWED;
20 import static android.app.AppOpsManager.MODE_DEFAULT;
21 import static android.app.AppOpsManager.MODE_IGNORED;
22 import static android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED;
23 import static android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM;
24 import static android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_UNKNOWN;
25 import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
26 
27 import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
28 import static com.android.settings.Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS;
29 
30 import android.app.AppOpsManager;
31 import android.apphibernation.AppHibernationManager;
32 import android.content.Context;
33 import android.content.pm.PackageManager;
34 import android.permission.PermissionControllerManager;
35 import android.provider.DeviceConfig;
36 import android.util.Slog;
37 
38 import androidx.annotation.NonNull;
39 import androidx.preference.Preference;
40 import androidx.preference.TwoStatePreference;
41 
42 import com.google.common.annotations.VisibleForTesting;
43 
44 /**
45  * A PreferenceController handling the logic for exempting hibernation of app
46  */
47 public final class HibernationSwitchPreferenceController extends AppInfoPreferenceControllerBase
48         implements Preference.OnPreferenceChangeListener {
49     private static final String TAG = "HibernationSwitchPrefController";
50     private String mPackageName;
51     private final AppOpsManager mAppOpsManager;
52     private final PermissionControllerManager mPermissionControllerManager;
53     private int mPackageUid;
54     private boolean mHibernationEligibilityLoaded;
55     private int mHibernationEligibility = HIBERNATION_ELIGIBILITY_UNKNOWN;
56     @VisibleForTesting
57     boolean mIsPackageSet;
58     private boolean mIsPackageExemptByDefault;
59 
HibernationSwitchPreferenceController(Context context, String preferenceKey)60     public HibernationSwitchPreferenceController(Context context,
61             String preferenceKey) {
62         super(context, preferenceKey);
63         mAppOpsManager = context.getSystemService(AppOpsManager.class);
64         mPermissionControllerManager = context.getSystemService(PermissionControllerManager.class);
65     }
66 
67     @Override
getAvailabilityStatus()68     public int getAvailabilityStatus() {
69         return isHibernationEnabled() && mIsPackageSet ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
70     }
71 
72     /**
73      * Set the package. And also retrieve details from package manager. Some packages may be
74      * exempted from hibernation by default. This method should only be called to initialize the
75      * controller.
76      * @param packageName The name of the package whose hibernation state to be managed.
77      */
setPackage(@onNull String packageName)78     void setPackage(@NonNull String packageName) {
79         mPackageName = packageName;
80         final PackageManager packageManager = mContext.getPackageManager();
81 
82         // Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R
83         final int maxTargetSdkVersionForExemptApps =
84                 packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
85                         ? android.os.Build.VERSION_CODES.R
86                         : android.os.Build.VERSION_CODES.Q;
87         try {
88             mPackageUid = packageManager.getPackageUid(packageName, /* flags */ 0);
89             mIsPackageExemptByDefault =
90                     hibernationTargetsPreSApps()
91                             ? false
92                             : packageManager.getTargetSdkVersion(packageName)
93                                     <= maxTargetSdkVersionForExemptApps;
94             mIsPackageSet = true;
95         } catch (PackageManager.NameNotFoundException e) {
96             Slog.w(TAG, "Package [" + mPackageName + "] is not found!");
97             mIsPackageSet = false;
98         }
99     }
100 
isAppEligibleForHibernation()101     private boolean isAppEligibleForHibernation() {
102         return mHibernationEligibilityLoaded
103                 && mHibernationEligibility != HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM
104                 && mHibernationEligibility != HIBERNATION_ELIGIBILITY_UNKNOWN;
105     }
106 
107     @Override
updateState(Preference preference)108     public void updateState(Preference preference) {
109         super.updateState(preference);
110         ((TwoStatePreference) preference).setChecked(isAppEligibleForHibernation()
111                 && !isPackageHibernationExemptByUser());
112         preference.setEnabled(isAppEligibleForHibernation());
113         if (!mHibernationEligibilityLoaded) {
114             mPermissionControllerManager.getHibernationEligibility(mPackageName,
115                     mContext.getMainExecutor(),
116                     eligibility -> {
117                         mHibernationEligibility = eligibility;
118                         mHibernationEligibilityLoaded = true;
119                         updateState(preference);
120                     });
121         }
122     }
123 
124     @VisibleForTesting
isPackageHibernationExemptByUser()125     boolean isPackageHibernationExemptByUser() {
126         if (!mIsPackageSet) return true;
127         final int mode = mAppOpsManager.unsafeCheckOpNoThrow(
128                 OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid, mPackageName);
129 
130         return mode == MODE_DEFAULT ? mIsPackageExemptByDefault : mode != MODE_ALLOWED;
131     }
132 
133     @Override
onPreferenceChange(Preference preference, Object isChecked)134     public boolean onPreferenceChange(Preference preference, Object isChecked) {
135         try {
136             final boolean checked = (boolean) isChecked;
137             mAppOpsManager.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid,
138                     checked ? MODE_ALLOWED : MODE_IGNORED);
139             if (!checked) {
140                 final AppHibernationManager ahm =
141                         mContext.getSystemService(AppHibernationManager.class);
142                 ahm.setHibernatingForUser(mPackageName, false);
143                 ahm.setHibernatingGlobally(mPackageName, false);
144             }
145         } catch (RuntimeException e) {
146             return false;
147         }
148         return true;
149     }
150 
isHibernationEnabled()151     private static boolean isHibernationEnabled() {
152         return DeviceConfig.getBoolean(
153                 NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true);
154     }
155 
hibernationTargetsPreSApps()156     private static boolean hibernationTargetsPreSApps() {
157         return DeviceConfig.getBoolean(
158                 NAMESPACE_APP_HIBERNATION, PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS, false);
159     }
160 }
161