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.fuelgauge;
18 
19 import android.annotation.IntDef;
20 import android.app.AppOpsManager;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.IPackageManager;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ParceledListSlice;
26 import android.content.pm.UserInfo;
27 import android.os.UserHandle;
28 import android.os.UserManager;
29 import android.util.ArraySet;
30 import android.util.Log;
31 
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.settings.R;
35 import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
36 import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils;
37 import com.android.settingslib.datastore.DataChangeReason;
38 import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
39 
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.Arrays;
43 import java.util.List;
44 
45 /** A utility class for application usage operation. */
46 public class BatteryOptimizeUtils {
47     private static final String TAG = "BatteryOptimizeUtils";
48     private static final String UNKNOWN_PACKAGE = "unknown";
49 
50     // Avoid reload the data again since it is predefined in the resource/config.
51     private static List<String> sBatteryOptimizeModeList = null;
52     private static List<String> sBatteryUnrestrictModeList = null;
53 
54     @VisibleForTesting AppOpsManager mAppOpsManager;
55     @VisibleForTesting BatteryUtils mBatteryUtils;
56     @VisibleForTesting PowerAllowlistBackend mPowerAllowListBackend;
57     @VisibleForTesting int mMode;
58     @VisibleForTesting boolean mAllowListed;
59 
60     private final String mPackageName;
61     private final Context mContext;
62     private final int mUid;
63 
64     // If current user is admin, match apps from all users. Otherwise, only match the currect user.
65     private static final int RETRIEVE_FLAG_ADMIN =
66             PackageManager.MATCH_ANY_USER
67                     | PackageManager.MATCH_DISABLED_COMPONENTS
68                     | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
69     private static final int RETRIEVE_FLAG =
70             PackageManager.MATCH_DISABLED_COMPONENTS
71                     | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
72 
73     // Optimization modes.
74     public static final int MODE_UNKNOWN = 0;
75     public static final int MODE_RESTRICTED = 1;
76     public static final int MODE_UNRESTRICTED = 2;
77     public static final int MODE_OPTIMIZED = 3;
78 
79     @IntDef(
80             prefix = {"MODE_"},
81             value = {
82                 MODE_UNKNOWN,
83                 MODE_RESTRICTED,
84                 MODE_UNRESTRICTED,
85                 MODE_OPTIMIZED,
86             })
87     @Retention(RetentionPolicy.SOURCE)
88     static @interface OptimizationMode {}
89 
BatteryOptimizeUtils(Context context, int uid, String packageName)90     public BatteryOptimizeUtils(Context context, int uid, String packageName) {
91         mUid = uid;
92         mContext = context;
93         mPackageName = packageName;
94         mAppOpsManager = context.getSystemService(AppOpsManager.class);
95         mBatteryUtils = BatteryUtils.getInstance(context);
96         mPowerAllowListBackend = PowerAllowlistBackend.getInstance(context);
97         mMode = getMode(mAppOpsManager, mUid, mPackageName);
98         mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName, mUid);
99     }
100 
101     /** Gets the {@link OptimizationMode} based on mode and allowed list. */
102     @OptimizationMode
getAppOptimizationMode(int mode, boolean isAllowListed)103     public static int getAppOptimizationMode(int mode, boolean isAllowListed) {
104         if (!isAllowListed && mode == AppOpsManager.MODE_IGNORED) {
105             return MODE_RESTRICTED;
106         } else if (isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
107             return MODE_UNRESTRICTED;
108         } else if (!isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
109             return MODE_OPTIMIZED;
110         } else {
111             return MODE_UNKNOWN;
112         }
113     }
114 
115     /** Gets the {@link OptimizationMode} for associated app. */
116     @OptimizationMode
getAppOptimizationMode(boolean refreshList)117     public int getAppOptimizationMode(boolean refreshList) {
118         if (refreshList) {
119             mPowerAllowListBackend.refreshList();
120         }
121         mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName, mUid);
122         mMode =
123                 mAppOpsManager.checkOpNoThrow(
124                         AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
125         Log.d(
126                 TAG,
127                 String.format(
128                         "refresh %s state, allowlisted = %s, mode = %d",
129                         mPackageName, mAllowListed, mMode));
130         return getAppOptimizationMode(mMode, mAllowListed);
131     }
132 
133     /** Gets the {@link OptimizationMode} for associated app. */
134     @OptimizationMode
getAppOptimizationMode()135     public int getAppOptimizationMode() {
136         return getAppOptimizationMode(true);
137     }
138 
139     /** Resets optimization mode for all applications. */
resetAppOptimizationMode( Context context, IPackageManager ipm, AppOpsManager aom)140     public static void resetAppOptimizationMode(
141             Context context, IPackageManager ipm, AppOpsManager aom) {
142         AppOptModeSharedPreferencesUtils.clearAll(context);
143         resetAppOptimizationModeInternal(
144                 context,
145                 ipm,
146                 aom,
147                 PowerAllowlistBackend.getInstance(context),
148                 BatteryUtils.getInstance(context));
149     }
150 
151     /** Sets the {@link OptimizationMode} for associated app. */
setAppUsageState(@ptimizationMode int mode, Action action)152     public void setAppUsageState(@OptimizationMode int mode, Action action) {
153         if (getAppOptimizationMode() == mode) {
154             Log.w(TAG, "set the same optimization mode for: " + mPackageName);
155             return;
156         }
157         setAppUsageStateInternal(
158                 mContext, mode, mUid, mPackageName, mBatteryUtils, mPowerAllowListBackend, action);
159     }
160 
161     /** Return {@code true} if it is disabled for default optimized mode only. */
isDisabledForOptimizeModeOnly()162     public boolean isDisabledForOptimizeModeOnly() {
163         return getForceBatteryOptimizeModeList(mContext).contains(mPackageName)
164                 || mBatteryUtils.getPackageUid(mPackageName) == BatteryUtils.UID_NULL;
165     }
166 
167     /** Return {@code true} if this package is system or default active app. */
isSystemOrDefaultApp()168     public boolean isSystemOrDefaultApp() {
169         mPowerAllowListBackend.refreshList();
170         return isSystemOrDefaultApp(mContext, mPowerAllowListBackend, mPackageName, mUid);
171     }
172 
173     /** Return {@code true} if the optimization mode of this package can be changed */
isOptimizeModeMutable()174     public boolean isOptimizeModeMutable() {
175         return !isDisabledForOptimizeModeOnly() && !isSystemOrDefaultApp();
176     }
177 
178     /**
179      * Return {@code true} if the optimization mode is mutable and current state is not restricted
180      */
isSelectorPreferenceEnabled()181     public boolean isSelectorPreferenceEnabled() {
182         // Enable the preference if apps are not set into restricted mode, otherwise disable it
183         return isOptimizeModeMutable()
184                 && getAppOptimizationMode() != BatteryOptimizeUtils.MODE_RESTRICTED;
185     }
186 
getPackageName()187     String getPackageName() {
188         return mPackageName == null ? UNKNOWN_PACKAGE : mPackageName;
189     }
190 
getUid()191     int getUid() {
192         return mUid;
193     }
194 
195     /** Gets the list of installed applications. */
getInstalledApplications( Context context, IPackageManager ipm)196     public static ArraySet<ApplicationInfo> getInstalledApplications(
197             Context context, IPackageManager ipm) {
198         final ArraySet<ApplicationInfo> applications = new ArraySet<>();
199         final UserManager um = context.getSystemService(UserManager.class);
200         for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
201             try {
202                 @SuppressWarnings("unchecked")
203                 final ParceledListSlice<ApplicationInfo> infoList =
204                         ipm.getInstalledApplications(
205                                 userInfo.isAdmin() ? RETRIEVE_FLAG_ADMIN : RETRIEVE_FLAG,
206                                 userInfo.id);
207                 if (infoList != null) {
208                     applications.addAll(infoList.getList());
209                 }
210             } catch (Exception e) {
211                 Log.e(TAG, "getInstalledApplications() is failed", e);
212                 return null;
213             }
214         }
215         // Removes the application which is disabled by the system.
216         applications.removeIf(
217                 info ->
218                         info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
219                                 && !info.enabled);
220         return applications;
221     }
222 
223     @VisibleForTesting
resetAppOptimizationModeInternal( Context context, IPackageManager ipm, AppOpsManager aom, PowerAllowlistBackend allowlistBackend, BatteryUtils batteryUtils)224     static void resetAppOptimizationModeInternal(
225             Context context,
226             IPackageManager ipm,
227             AppOpsManager aom,
228             PowerAllowlistBackend allowlistBackend,
229             BatteryUtils batteryUtils) {
230         final ArraySet<ApplicationInfo> applications = getInstalledApplications(context, ipm);
231         if (applications == null || applications.isEmpty()) {
232             Log.w(TAG, "no data found in the getInstalledApplications()");
233             return;
234         }
235 
236         // App preferences are already clear when code reach here, and there may be no
237         // setAppUsageStateInternal call to notifyChange. So always trigger notifyChange here.
238         BatterySettingsStorage.get(context).notifyChange(DataChangeReason.DELETE);
239 
240         allowlistBackend.refreshList();
241         // Resets optimization mode for each application.
242         for (ApplicationInfo info : applications) {
243             final int mode =
244                     aom.checkOpNoThrow(
245                             AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName);
246             @OptimizationMode
247             final int optimizationMode =
248                     getAppOptimizationMode(
249                             mode, allowlistBackend.isAllowlisted(info.packageName, info.uid));
250             // Ignores default optimized/unknown state or system/default apps.
251             if (optimizationMode == MODE_OPTIMIZED
252                     || optimizationMode == MODE_UNKNOWN
253                     || isSystemOrDefaultApp(
254                             context, allowlistBackend, info.packageName, info.uid)) {
255                 continue;
256             }
257 
258             // Resets to the default mode: MODE_OPTIMIZED.
259             setAppUsageStateInternal(
260                     context,
261                     MODE_OPTIMIZED,
262                     info.uid,
263                     info.packageName,
264                     batteryUtils,
265                     allowlistBackend,
266                     Action.RESET);
267         }
268     }
269 
getMode(AppOpsManager appOpsManager, int uid, String packageName)270     static int getMode(AppOpsManager appOpsManager, int uid, String packageName) {
271         return appOpsManager.checkOpNoThrow(
272                 AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
273     }
274 
isSystemOrDefaultApp( Context context, PowerAllowlistBackend powerAllowlistBackend, String packageName, int uid)275     static boolean isSystemOrDefaultApp(
276             Context context,
277             PowerAllowlistBackend powerAllowlistBackend,
278             String packageName,
279             int uid) {
280         return powerAllowlistBackend.isSysAllowlisted(packageName)
281                 // Always forced unrestricted apps are one type of system important apps.
282                 || getForceBatteryUnrestrictModeList(context).contains(packageName)
283                 || powerAllowlistBackend.isDefaultActiveApp(packageName, uid);
284     }
285 
getForceBatteryOptimizeModeList(Context context)286     static List<String> getForceBatteryOptimizeModeList(Context context) {
287         if (sBatteryOptimizeModeList == null) {
288             sBatteryOptimizeModeList =
289                     Arrays.asList(
290                             context.getResources()
291                                     .getStringArray(
292                                             R.array.config_force_battery_optimize_mode_apps));
293         }
294         return sBatteryOptimizeModeList;
295     }
296 
getForceBatteryUnrestrictModeList(Context context)297     static List<String> getForceBatteryUnrestrictModeList(Context context) {
298         if (sBatteryUnrestrictModeList == null) {
299             sBatteryUnrestrictModeList =
300                     Arrays.asList(
301                             context.getResources()
302                                     .getStringArray(
303                                             R.array.config_force_battery_unrestrict_mode_apps));
304         }
305         return sBatteryUnrestrictModeList;
306     }
307 
setAppUsageStateInternal( Context context, @OptimizationMode int mode, int uid, String packageName, BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend, Action action)308     private static void setAppUsageStateInternal(
309             Context context,
310             @OptimizationMode int mode,
311             int uid,
312             String packageName,
313             BatteryUtils batteryUtils,
314             PowerAllowlistBackend powerAllowlistBackend,
315             Action action) {
316         if (mode == MODE_UNKNOWN) {
317             Log.d(TAG, "set unknown app optimization mode.");
318             return;
319         }
320 
321         // MODE_RESTRICTED = AppOpsManager.MODE_IGNORED + !allowListed
322         // MODE_UNRESTRICTED = AppOpsManager.MODE_ALLOWED + allowListed
323         // MODE_OPTIMIZED = AppOpsManager.MODE_ALLOWED + !allowListed
324         final int appOpsManagerMode =
325                 mode == MODE_RESTRICTED ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
326         final boolean allowListed = mode == MODE_UNRESTRICTED;
327 
328         setAppOptimizationModeInternal(
329                 context,
330                 appOpsManagerMode,
331                 allowListed,
332                 uid,
333                 packageName,
334                 batteryUtils,
335                 powerAllowlistBackend,
336                 action);
337     }
338 
setAppOptimizationModeInternal( Context context, int appStandbyMode, boolean allowListed, int uid, String packageName, BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend, Action action)339     private static void setAppOptimizationModeInternal(
340             Context context,
341             int appStandbyMode,
342             boolean allowListed,
343             int uid,
344             String packageName,
345             BatteryUtils batteryUtils,
346             PowerAllowlistBackend powerAllowlistBackend,
347             Action action) {
348         final String packageNameKey =
349                 BatteryOptimizeLogUtils.getPackageNameWithUserId(
350                         packageName, UserHandle.myUserId());
351         try {
352             batteryUtils.setForceAppStandby(uid, packageName, appStandbyMode);
353             if (allowListed) {
354                 powerAllowlistBackend.addApp(packageName, uid);
355             } else {
356                 powerAllowlistBackend.removeApp(packageName, uid);
357             }
358         } catch (Exception e) {
359             // Error cases, set standby mode as -1 for logging.
360             appStandbyMode = -1;
361             Log.e(TAG, "set OPTIMIZATION MODE failed for " + packageName, e);
362         }
363         BatteryOptimizeLogUtils.writeLog(
364                 context, action, packageNameKey, createLogEvent(appStandbyMode, allowListed));
365         if (action != Action.RESET) { // reset has been notified in resetAppOptimizationMode
366             BatterySettingsStorage.get(context).notifyChange(toChangeReason(action));
367         }
368     }
369 
createLogEvent(int appStandbyMode, boolean allowListed)370     private static String createLogEvent(int appStandbyMode, boolean allowListed) {
371         return appStandbyMode < 0
372                 ? "Apply optimize setting ERROR"
373                 : String.format(
374                         "\tStandbyMode: %s, allowListed: %s, mode: %s",
375                         appStandbyMode,
376                         allowListed,
377                         getAppOptimizationMode(appStandbyMode, allowListed));
378     }
379 
toChangeReason(Action action)380     private static @DataChangeReason int toChangeReason(Action action) {
381         return action == Action.RESTORE ? DataChangeReason.RESTORE : DataChangeReason.UPDATE;
382     }
383 }
384