1 /*
2  * Copyright (C) 2023 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.tv.settings.device.eco;
18 
19 import android.annotation.ArrayRes;
20 import android.annotation.BoolRes;
21 import android.annotation.ColorRes;
22 import android.annotation.DrawableRes;
23 import android.annotation.IntegerRes;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.StringRes;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.os.PowerManager;
30 import android.os.PowerManager.LowPowerStandbyPolicy;
31 import android.provider.DeviceConfig;
32 import android.text.TextUtils;
33 import android.util.ArraySet;
34 
35 import com.android.tv.settings.R;
36 import com.android.tv.settings.overlay.FlavorUtils;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Set;
42 
43 /**
44  * Provides available energy modes, and allows to update the current energy mode.
45  */
46 public final class EnergyModesHelper {
47     public static final String NAMESPACE_LOW_POWER_STANDBY = "low_power_standby";
48     public static final String KEY_ENABLE_POLICY = "enable_policy";
49     private static final String LIST_ITEM_BULLET = "\u2022 ";
50 
51     private final Context mContext;
52 
53     /** Describes an Energy Mode. */
54     public static final class EnergyMode {
55         @StringRes
56         public final int identifierRes;
57         public final boolean ecoHighlighted;
58         public final boolean enableLowPowerStandby;
59         @BoolRes
60         public final int enabledRes;
61         @StringRes
62         public final int titleRes;
63         @StringRes
64         public final int subtitleRes;
65         @ColorRes
66         public final int colorRes;
67         @DrawableRes
68         public final int iconRes;
69         @StringRes
70         public final int infoTextRes;
71         @ArrayRes
72         public final int featuresRes;
73         @StringRes
74         public final int ecoHintRes;
75         @DrawableRes
76         public final int ecoHintIconRes;
77 
78         @ArrayRes
79         public final int baseExemptPackagesRes;
80         @ArrayRes
81         public final int vendorExemptPackagesRes;
82         @IntegerRes
83         public final int baseAllowedReasonsRes;
84         @IntegerRes
85         public final int vendorAllowedReasonsRes;
86         @ArrayRes
87         public final int baseAllowedFeaturesRes;
88         @ArrayRes
89         public final int vendorAllowedFeaturesRes;
90 
91         /**
92          * Base mode from which all allowed reasons, allowed features, and exempt packages
93          * will be inherited.
94          */
95         @Nullable
96         public final EnergyMode baseMode;
97 
EnergyMode(@tringRes int identifierRes, boolean ecoHighlighted, boolean enableLowPowerStandby, @BoolRes int enabledRes, @StringRes int titleRes, @StringRes int subtitleRes, int colorRes, @DrawableRes int iconRes, @StringRes int infoTextRes, @ArrayRes int featuresRes, @StringRes int ecoHintRes, @DrawableRes int ecoHintIconRes, @ArrayRes int baseExemptPackagesRes, @ArrayRes int vendorExemptPackagesRes, @IntegerRes int baseAllowedReasonsRes, @IntegerRes int vendorAllowedReasonsRes, @ArrayRes int baseAllowedFeaturesRes, @ArrayRes int vendorAllowedFeaturesRes, @Nullable EnergyMode baseMode)98         public EnergyMode(@StringRes int identifierRes, boolean ecoHighlighted,
99                 boolean enableLowPowerStandby, @BoolRes int enabledRes, @StringRes int titleRes,
100                 @StringRes int subtitleRes, int colorRes, @DrawableRes int iconRes,
101                 @StringRes int infoTextRes, @ArrayRes int featuresRes, @StringRes int ecoHintRes,
102                 @DrawableRes int ecoHintIconRes, @ArrayRes int baseExemptPackagesRes,
103                 @ArrayRes int vendorExemptPackagesRes, @IntegerRes int baseAllowedReasonsRes,
104                 @IntegerRes int vendorAllowedReasonsRes, @ArrayRes int baseAllowedFeaturesRes,
105                 @ArrayRes int vendorAllowedFeaturesRes, @Nullable EnergyMode baseMode) {
106             this.ecoHighlighted = ecoHighlighted;
107             this.enableLowPowerStandby = enableLowPowerStandby;
108             this.enabledRes = enabledRes;
109             this.titleRes = titleRes;
110             this.subtitleRes = subtitleRes;
111             this.colorRes = colorRes;
112             this.iconRes = iconRes;
113             this.infoTextRes = infoTextRes;
114             this.featuresRes = featuresRes;
115             this.ecoHintRes = ecoHintRes;
116             this.ecoHintIconRes = ecoHintIconRes;
117             this.identifierRes = identifierRes;
118             this.baseExemptPackagesRes = baseExemptPackagesRes;
119             this.vendorExemptPackagesRes = vendorExemptPackagesRes;
120             this.baseAllowedReasonsRes = baseAllowedReasonsRes;
121             this.vendorAllowedReasonsRes = vendorAllowedReasonsRes;
122             this.baseAllowedFeaturesRes = baseAllowedFeaturesRes;
123             this.vendorAllowedFeaturesRes = vendorAllowedFeaturesRes;
124             this.baseMode = baseMode;
125         }
126     }
127 
128     public static EnergyMode MODE_LOW_ENERGY = new EnergyMode(
129             R.string.energy_mode_low_identifier,
130             /* ecoHighlighted= */ true,
131             /* enableLowPowerStandby= */ true,
132             R.bool.energy_mode_low_enabled,
133             R.string.energy_mode_low_title,
134             R.string.energy_mode_low_subtitle,
135             R.color.energy_mode_low_color,
136             R.drawable.energy_mode_low_icon,
137             R.string.energy_mode_low_info,
138             R.array.energy_mode_low_features,
139             R.string.energy_mode_low_eco_hint,
140             R.drawable.ic_tips_and_updates,
141             R.array.energy_mode_low_baseExemptPackages,
142             R.array.energy_mode_low_vendorExemptPackages,
143             R.integer.energy_mode_low_baseAllowedReasons,
144             R.integer.energy_mode_low_vendorAllowedReasons,
145             R.array.energy_mode_low_baseAllowedFeatures,
146             R.array.energy_mode_low_vendorAllowedFeatures,
147             /* baseMode= */ null);
148 
149     public static EnergyMode MODE_MODERATE_ENERGY = new EnergyMode(
150             R.string.energy_mode_moderate_identifier,
151             /* ecoHighlighted= */ false,
152             /* enableLowPowerStandby= */ true,
153             R.bool.energy_mode_moderate_enabled,
154             R.string.energy_mode_moderate_title,
155             R.string.energy_mode_moderate_subtitle,
156             R.color.energy_mode_moderate_color,
157             R.drawable.energy_mode_moderate_icon,
158             R.string.energy_mode_moderate_info,
159             R.array.energy_mode_moderate_features,
160             R.string.energy_mode_moderate_eco_hint,
161             /* ecoHintIconRes= */ 0,
162             R.array.energy_mode_moderate_baseExemptPackages,
163             R.array.energy_mode_moderate_vendorExemptPackages,
164             R.integer.energy_mode_moderate_baseAllowedReasons,
165             R.integer.energy_mode_moderate_vendorAllowedReasons,
166             R.array.energy_mode_moderate_baseAllowedFeatures,
167             R.array.energy_mode_moderate_vendorAllowedFeatures,
168             MODE_LOW_ENERGY);
169 
170     public static EnergyMode MODE_HIGH_ENERGY = new EnergyMode(
171             R.string.energy_mode_high_identifier,
172             /* ecoHighlighted= */ false,
173             /* enableLowPowerStandby= */ true,
174             R.bool.energy_mode_high_enabled,
175             R.string.energy_mode_high_title,
176             R.string.energy_mode_high_subtitle,
177             R.color.energy_mode_high_color,
178             R.drawable.energy_mode_high_icon,
179             R.string.energy_mode_high_info,
180             R.array.energy_mode_high_features,
181             R.string.energy_mode_high_eco_hint,
182             R.drawable.ic_bolt,
183             R.array.energy_mode_high_baseExemptPackages,
184             R.array.energy_mode_high_vendorExemptPackages,
185             R.integer.energy_mode_high_baseAllowedReasons,
186             R.integer.energy_mode_high_vendorAllowedReasons,
187             R.array.energy_mode_high_baseAllowedFeatures,
188             R.array.energy_mode_high_vendorAllowedFeatures,
189             MODE_MODERATE_ENERGY);
190 
191     public static EnergyMode MODE_UNRESTRICTED = new EnergyMode(
192             R.string.energy_mode_unrestricted_identifier,
193             false,
194             false,
195             R.bool.energy_mode_unrestricted_enabled,
196             R.string.energy_mode_high_title,
197             R.string.energy_mode_high_subtitle,
198             R.color.energy_mode_high_color,
199             R.drawable.energy_mode_high_icon,
200             R.string.energy_mode_high_info,
201             R.array.energy_mode_high_features,
202             R.string.energy_mode_high_eco_hint,
203             R.drawable.ic_bolt,
204             0, 0, 0, 0, 0, 0, null);
205 
206     public static EnergyMode[] ENERGY_MODES = new EnergyMode[] {
207             MODE_LOW_ENERGY, MODE_MODERATE_ENERGY, MODE_HIGH_ENERGY, MODE_UNRESTRICTED };
208 
EnergyModesHelper(Context context)209     public EnergyModesHelper(Context context) {
210         mContext = context;
211     }
212 
213     /**
214      * Returns whether this device supports Low Power Standby.
215      *
216      * If false, energy modes are not supported.
217      */
isLowPowerStandbySupported(Context context)218     public static boolean isLowPowerStandbySupported(Context context) {
219         if (FlavorUtils.getFeatureFactory(context).getBasicModeFeatureProvider()
220                 .isBasicMode(context)) {
221             return false; // Basic mode does no background processing during standby.
222         }
223         final PowerManager powerManager = context.getSystemService(PowerManager.class);
224         return powerManager.isLowPowerStandbySupported();
225     }
226 
areEnergyModesEnabled()227     private boolean areEnergyModesEnabled() {
228         boolean enableEnergyModes = mContext.getResources().getBoolean(R.bool.enable_energy_modes);
229         boolean customPoliciesEnabled = DeviceConfig.getBoolean(NAMESPACE_LOW_POWER_STANDBY,
230                 KEY_ENABLE_POLICY, true);
231 
232         return enableEnergyModes && customPoliciesEnabled && isLowPowerStandbySupported(mContext);
233     }
234 
235     /** Returns whether Energy Modes should be shown and used on this device */
areEnergyModesAvailable()236     public boolean areEnergyModesAvailable() {
237         return !getEnergyModes().isEmpty();
238     }
239 
240     /** Returns all enabled energy modes in the order they should be presented. */
241     @NonNull
getEnergyModes()242     public List<EnergyMode> getEnergyModes() {
243         ArrayList<EnergyMode> enabledModes = new ArrayList<>();
244         if (!areEnergyModesEnabled()) {
245             return enabledModes;
246         }
247 
248         if (isEnergyModeEnabled(MODE_LOW_ENERGY)) {
249             enabledModes.add(MODE_LOW_ENERGY);
250         }
251 
252         if (isEnergyModeEnabled(MODE_MODERATE_ENERGY)) {
253             enabledModes.add(MODE_MODERATE_ENERGY);
254         }
255 
256         if (isEnergyModeEnabled(MODE_UNRESTRICTED)) {
257             enabledModes.add(MODE_UNRESTRICTED);
258         } else if (isEnergyModeEnabled(MODE_HIGH_ENERGY)) {
259             enabledModes.add(MODE_HIGH_ENERGY);
260         }
261 
262         return enabledModes;
263     }
264 
isEnergyModeEnabled(EnergyMode mode)265     private boolean isEnergyModeEnabled(EnergyMode mode) {
266         if (mode == null) {
267             return false;
268         }
269 
270         if (mode == MODE_HIGH_ENERGY && isEnergyModeEnabled(MODE_UNRESTRICTED)) {
271             // unrestricted mode overrides high energy mode
272             return false;
273         }
274 
275         Resources resources = mContext.getResources();
276         boolean baseEnabled = resources.getBoolean(mode.enabledRes);
277         String identifier = mContext.getString(mode.identifierRes);
278         return DeviceConfig.getBoolean(NAMESPACE_LOW_POWER_STANDBY,
279                 "policy_" + identifier + "_enabled", baseEnabled);
280     }
281 
282     /** Returns an energy mode by its identifier, or null if not found. */
283     @Nullable
getEnergyMode(@tringRes int identifierRes)284     public EnergyMode getEnergyMode(@StringRes int identifierRes) {
285         for (EnergyMode energyMode : ENERGY_MODES) {
286             if (energyMode.identifierRes == identifierRes) {
287                 return energyMode;
288             }
289         }
290 
291         return null;
292     }
293 
294     /** Returns an energy mode by its identifier, or null if not found. */
295     @Nullable
getEnergyMode(String identifier)296     public EnergyMode getEnergyMode(String identifier) {
297         for (EnergyMode energyMode : ENERGY_MODES) {
298             if (mContext.getString(energyMode.identifierRes).equals(identifier)) {
299                 return energyMode;
300             }
301         }
302 
303         return null;
304     }
305 
306     /** Returns the description of the energy mode, incl. list of features */
getSummary(EnergyMode mode)307     public String getSummary(EnergyMode mode) {
308         StringBuilder summary = new StringBuilder();
309         summary.append(mContext.getString(mode.infoTextRes));
310 
311         String featuresList = getFeaturesList(mode);
312         if (featuresList != null) {
313             summary.append("\n\n");
314             summary.append(mContext.getString(R.string.energy_mode_enables));
315             summary.append("\n");
316             summary.append(featuresList);
317         }
318 
319         return summary.toString();
320     }
321 
322     /** Returns the list of features formatted for display in the Settings UI */
323     @Nullable
getFeaturesList(EnergyMode mode)324     public String getFeaturesList(EnergyMode mode) {
325         String[] features = mContext.getResources().getStringArray(mode.featuresRes);
326         if (features.length == 0) {
327             return null;
328         }
329 
330         StringBuilder featureList = new StringBuilder();
331 
332         for (int i = 0; i < features.length; i++) {
333             featureList.append(LIST_ITEM_BULLET);
334             featureList.append(features[i]);
335             if (i < features.length - 1) {
336                 featureList.append("\n");
337             }
338         }
339 
340         return featureList.toString();
341     }
342 
343     @Nullable
getDeviceConfigStringArray(String key)344     private String[] getDeviceConfigStringArray(String key) {
345         String string = DeviceConfig.getString(NAMESPACE_LOW_POWER_STANDBY, key, null);
346         if (string == null) {
347             return null;
348         }
349         return string.split(",");
350     }
351 
getPolicy(EnergyMode mode)352     LowPowerStandbyPolicy getPolicy(EnergyMode mode) {
353         if (!mode.enableLowPowerStandby) {
354             return new LowPowerStandbyPolicy(
355                     mContext.getString(mode.identifierRes),
356                     Collections.emptySet(),
357                     0,
358                     Collections.emptySet());
359         }
360 
361         return new LowPowerStandbyPolicy(
362                 mContext.getString(mode.identifierRes),
363                 getExemptPackages(mode),
364                 getAllowedReasons(mode),
365                 getAllowedFeatures(mode));
366     }
367 
368     @NonNull
getExemptPackages(@onNull EnergyMode mode)369     private Set<String> getExemptPackages(@NonNull EnergyMode mode) {
370         final String identifier = mContext.getString(mode.identifierRes);
371         final Set<String> exemptPackages = combineStringArrays(mode.baseExemptPackagesRes,
372                 "policy_" + identifier + "_exempt_packages", mode.vendorExemptPackagesRes);
373 
374         if (mode.baseMode != null) {
375             exemptPackages.addAll(getExemptPackages(mode.baseMode));
376         }
377 
378         return exemptPackages;
379     }
380 
381     @NonNull
getAllowedFeatures(@onNull EnergyMode mode)382     Set<String> getAllowedFeatures(@NonNull EnergyMode mode) {
383         final String identifier = mContext.getString(mode.identifierRes);
384         final Set<String> allowedFeatures = combineStringArrays(mode.baseAllowedFeaturesRes,
385                 "policy_" + identifier + "_allowed_features", mode.vendorAllowedFeaturesRes);
386 
387         if (mode.baseMode != null) {
388             allowedFeatures.addAll(getAllowedFeatures(mode.baseMode));
389         }
390 
391         return allowedFeatures;
392     }
393 
combineStringArrays(@rrayRes int baseArrayRes, String baseOverrideKey, @ArrayRes int vendorArrayRes)394     private Set<String> combineStringArrays(@ArrayRes int baseArrayRes, String baseOverrideKey,
395             @ArrayRes int vendorArrayRes) {
396         final Resources resources = mContext.getResources();
397         final String[] baseArray = resources.getStringArray(baseArrayRes);
398         final String[] baseOverrideArray = getDeviceConfigStringArray(baseOverrideKey);
399         final String[] vendorArray = resources.getStringArray(vendorArrayRes);
400 
401         ArraySet<String> result = new ArraySet<>();
402         result.addAll(new ArraySet<>(baseOverrideArray != null
403                 ? baseOverrideArray
404                 : baseArray));
405         result.addAll(new ArraySet<>(vendorArray));
406         return result;
407     }
408 
getAllowedReasons(@onNull EnergyMode mode)409     private int getAllowedReasons(@NonNull EnergyMode mode) {
410         final Resources resources = mContext.getResources();
411         final String identifier = mContext.getString(mode.identifierRes);
412 
413         final int baseAllowedReasons = resources.getInteger(mode.baseAllowedReasonsRes);
414         final int deviceConfigAllowedReasonOverride = DeviceConfig.getInt(
415                 NAMESPACE_LOW_POWER_STANDBY, "policy_" + identifier + "_allowed_reasons", -1);
416         final int vendorAllowedReasons = resources.getInteger(mode.vendorAllowedReasonsRes);
417         int allowedReasons = ((deviceConfigAllowedReasonOverride != -1
418                 ? deviceConfigAllowedReasonOverride
419                 : baseAllowedReasons) | vendorAllowedReasons);
420 
421         if (mode.baseMode != null) {
422             allowedReasons |= getAllowedReasons(mode.baseMode);
423         }
424 
425         return allowedReasons;
426     }
427 
428     /** Sets the given energy mode in the system. */
setEnergyMode(@onNull EnergyMode energyMode)429     public void setEnergyMode(@NonNull EnergyMode energyMode) {
430         LowPowerStandbyPolicy policy = getPolicy(energyMode);
431         PowerManager powerManager = mContext.getSystemService(PowerManager.class);
432         powerManager.setLowPowerStandbyEnabled(energyMode.enableLowPowerStandby);
433         powerManager.setLowPowerStandbyPolicy(policy);
434     }
435 
436     /**
437      * Returns the default energy mode.
438      *
439      * This energy mode is used if the current Low Power Standby policy doesn't match any valid
440      * and enabled energy modes.
441      */
442     @Nullable
getDefaultEnergyMode()443     public EnergyMode getDefaultEnergyMode() {
444         if (!areEnergyModesAvailable()) {
445             return null;
446         }
447         return getEnergyMode(mContext.getString(R.string.default_energy_mode));
448     }
449 
450     /**
451      * Returns true if going from the current energy mode to the given energy mode requires
452      * the user to confirm the change.
453      */
requiresConfirmation(EnergyMode currentMode, EnergyMode newMode)454     public boolean requiresConfirmation(EnergyMode currentMode, EnergyMode newMode) {
455         int currentModeIndex = getEnergyModeIndex(currentMode);
456         int newModeIndex = getEnergyModeIndex(newMode);
457 
458         if (currentModeIndex == -1) {
459             return newModeIndex > 0;
460         }
461 
462         return newModeIndex > currentModeIndex;
463     }
464 
getEnergyModeIndex(EnergyMode mode)465     private int getEnergyModeIndex(EnergyMode mode) {
466         if (mode == null) {
467             return -1;
468         }
469         for (int i = 0; i < ENERGY_MODES.length; i++) {
470             if (mode == ENERGY_MODES[i]) {
471                 return i;
472             }
473         }
474         return -1;
475     }
476 
477     /**
478      * Makes sure a valid energy mode is set, if energy modes are enabled, and returns the current
479      * energy mode.
480      */
481     @Nullable
updateEnergyMode()482     public EnergyMode updateEnergyMode() {
483         if (!areEnergyModesAvailable()) {
484             return null;
485         }
486 
487         PowerManager powerManager = mContext.getSystemService(PowerManager.class);
488         final LowPowerStandbyPolicy currentPolicy = powerManager.getLowPowerStandbyPolicy();
489         if (currentPolicy == null) {
490             return null;
491         }
492 
493         final EnergyMode matchingEnergyMode = getEnergyMode(currentPolicy.getIdentifier());
494         EnergyMode targetEnergyMode = matchingEnergyMode;
495         if (!isEnergyModeEnabled(matchingEnergyMode)) {
496             if (matchingEnergyMode == MODE_HIGH_ENERGY && isEnergyModeEnabled(MODE_UNRESTRICTED)) {
497                 targetEnergyMode = MODE_UNRESTRICTED;
498             } else if (matchingEnergyMode == MODE_UNRESTRICTED && isEnergyModeEnabled(
499                     MODE_HIGH_ENERGY)) {
500                 targetEnergyMode = MODE_HIGH_ENERGY;
501             } else {
502                 targetEnergyMode = getDefaultEnergyMode();
503                 if (targetEnergyMode == null) {
504                     // Fall back to lowest energy mode if default is not set or invalid
505                     targetEnergyMode = getEnergyModes().get(0);
506                 }
507             }
508         }
509 
510         setEnergyMode(targetEnergyMode);
511         return targetEnergyMode;
512     }
513 }
514