1 package org.robolectric.shadows;
2 
3 import static android.content.Intent.ACTION_SCREEN_OFF;
4 import static android.content.Intent.ACTION_SCREEN_ON;
5 import static android.os.Build.VERSION_CODES.M;
6 import static android.os.Build.VERSION_CODES.N;
7 import static android.os.Build.VERSION_CODES.O;
8 import static android.os.Build.VERSION_CODES.P;
9 import static android.os.Build.VERSION_CODES.Q;
10 import static android.os.Build.VERSION_CODES.R;
11 import static android.os.Build.VERSION_CODES.S;
12 import static android.os.Build.VERSION_CODES.TIRAMISU;
13 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
14 import static com.google.common.base.Preconditions.checkState;
15 import static java.util.Comparator.comparing;
16 import static java.util.stream.Collectors.toCollection;
17 import static org.robolectric.util.reflector.Reflector.reflector;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.RequiresPermission;
22 import android.annotation.SystemApi;
23 import android.annotation.TargetApi;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.Binder;
27 import android.os.PowerManager;
28 import android.os.PowerManager.LowPowerStandbyPortDescription;
29 import android.os.PowerManager.LowPowerStandbyPortsLock;
30 import android.os.PowerManager.WakeLock;
31 import android.os.SystemClock;
32 import android.os.WorkSource;
33 import com.google.common.collect.ImmutableSet;
34 import java.time.Duration;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Optional;
42 import java.util.Set;
43 import org.robolectric.RuntimeEnvironment;
44 import org.robolectric.annotation.ClassName;
45 import org.robolectric.annotation.HiddenApi;
46 import org.robolectric.annotation.Implementation;
47 import org.robolectric.annotation.Implements;
48 import org.robolectric.annotation.RealObject;
49 import org.robolectric.annotation.Resetter;
50 import org.robolectric.shadow.api.Shadow;
51 import org.robolectric.util.reflector.Accessor;
52 import org.robolectric.util.reflector.Direct;
53 import org.robolectric.util.reflector.ForType;
54 
55 /** Shadow of PowerManager */
56 @Implements(value = PowerManager.class)
57 public class ShadowPowerManager {
58 
59   @RealObject private PowerManager realPowerManager;
60 
61   private static boolean isInteractive = true;
62   private static boolean isPowerSaveMode = false;
63   private static boolean isDeviceIdleMode = false;
64   private static boolean isLightDeviceIdleMode = false;
65   @Nullable private static Duration batteryDischargePrediction = null;
66   private static boolean isBatteryDischargePredictionPersonalized = false;
67 
68   @PowerManager.LocationPowerSaveMode
69   private static int locationMode = PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF;
70 
71   private static final List<String> rebootReasons = new ArrayList<>();
72   private static final Map<String, Boolean> ignoringBatteryOptimizations = new HashMap<>();
73 
74   private static int thermalStatus = 0;
75   // Intentionally use Object instead of PowerManager.OnThermalStatusChangedListener to avoid
76   // ClassLoader exceptions on earlier SDKs that don't have this class.
77   private static final Set<Object> thermalListeners = new HashSet<>();
78 
79   private static final Set<String> ambientDisplaySuppressionTokens =
80       Collections.synchronizedSet(new HashSet<>());
81   private static volatile boolean isAmbientDisplayAvailable = true;
82   private static volatile boolean isRebootingUserspaceSupported = false;
83   private static volatile boolean adaptivePowerSaveEnabled = false;
84 
85   private static PowerManager.WakeLock latestWakeLock;
86 
87   private static boolean lowPowerStandbyEnabled = false;
88   private static boolean lowPowerStandbySupported = false;
89   private static boolean exemptFromLowPowerStandby = false;
90   private static final Set<String> allowedFeatures = new HashSet<String>();
91 
92   @Implementation
newWakeLock(int flags, String tag)93   protected PowerManager.WakeLock newWakeLock(int flags, String tag) {
94     PowerManager.WakeLock wl =
95         reflector(PowerManagerReflector.class, realPowerManager).newWakeLock(flags, tag);
96     latestWakeLock = wl;
97     return wl;
98   }
99 
100   @Implementation
isScreenOn()101   protected boolean isScreenOn() {
102     return isInteractive;
103   }
104 
105   /**
106    * @deprecated Use {@link #turnScreenOn(boolean)} instead.
107    */
108   @Deprecated
setIsScreenOn(boolean screenOn)109   public void setIsScreenOn(boolean screenOn) {
110     setIsInteractive(screenOn);
111   }
112 
113   @Implementation
isInteractive()114   protected boolean isInteractive() {
115     return isInteractive;
116   }
117 
118   /**
119    * @deprecated Prefer {@link #turnScreenOn(boolean)} instead.
120    */
121   @Deprecated
setIsInteractive(boolean interactive)122   public void setIsInteractive(boolean interactive) {
123     isInteractive = interactive;
124   }
125 
126   /** Emulates turning the screen on/off if the screen is not already on/off. */
turnScreenOn(boolean screenOn)127   public void turnScreenOn(boolean screenOn) {
128     if (isInteractive != screenOn) {
129       isInteractive = screenOn;
130       getContext().sendBroadcast(new Intent(screenOn ? ACTION_SCREEN_ON : ACTION_SCREEN_OFF));
131     }
132   }
133 
134   @Implementation
isPowerSaveMode()135   protected boolean isPowerSaveMode() {
136     return isPowerSaveMode;
137   }
138 
setIsPowerSaveMode(boolean powerSaveMode)139   public void setIsPowerSaveMode(boolean powerSaveMode) {
140     isPowerSaveMode = powerSaveMode;
141   }
142 
143   private Map<Integer, Boolean> supportedWakeLockLevels = new HashMap<>();
144 
145   @Implementation
isWakeLockLevelSupported(int level)146   protected boolean isWakeLockLevelSupported(int level) {
147     return supportedWakeLockLevels.containsKey(level) ? supportedWakeLockLevels.get(level) : false;
148   }
149 
setIsWakeLockLevelSupported(int level, boolean supported)150   public void setIsWakeLockLevelSupported(int level, boolean supported) {
151     supportedWakeLockLevels.put(level, supported);
152   }
153 
154   /**
155    * @return false by default, or the value specified via {@link #setIsDeviceIdleMode(boolean)}
156    */
157   @Implementation(minSdk = M)
isDeviceIdleMode()158   protected boolean isDeviceIdleMode() {
159     return isDeviceIdleMode;
160   }
161 
162   /** Sets the value returned by {@link #isDeviceIdleMode()}. */
setIsDeviceIdleMode(boolean isDeviceIdleMode)163   public void setIsDeviceIdleMode(boolean isDeviceIdleMode) {
164     this.isDeviceIdleMode = isDeviceIdleMode;
165     getContext().sendBroadcast(new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
166   }
167 
168   /**
169    * @return false by default, or the value specified via {@link #setIsLightDeviceIdleMode(boolean)}
170    */
171   @Implementation(minSdk = N)
isLightDeviceIdleMode()172   protected boolean isLightDeviceIdleMode() {
173     return isLightDeviceIdleMode;
174   }
175 
176   /** Sets the value returned by {@link #isLightDeviceIdleMode()}. */
setIsLightDeviceIdleMode(boolean lightDeviceIdleMode)177   public void setIsLightDeviceIdleMode(boolean lightDeviceIdleMode) {
178     isLightDeviceIdleMode = lightDeviceIdleMode;
179   }
180 
181   @Implementation(minSdk = TIRAMISU)
isDeviceLightIdleMode()182   protected boolean isDeviceLightIdleMode() {
183     return isLightDeviceIdleMode();
184   }
185 
186   /** Sets the value returned by {@link #isDeviceLightIdleMode()}. */
setIsDeviceLightIdleMode(boolean lightDeviceIdleMode)187   public void setIsDeviceLightIdleMode(boolean lightDeviceIdleMode) {
188     setIsLightDeviceIdleMode(lightDeviceIdleMode);
189   }
190 
191   /**
192    * Returns how location features should behave when battery saver is on. When battery saver is
193    * off, this will always return {@link #LOCATION_MODE_NO_CHANGE}.
194    */
195   @Implementation(minSdk = P)
196   @PowerManager.LocationPowerSaveMode
getLocationPowerSaveMode()197   protected int getLocationPowerSaveMode() {
198     if (!isPowerSaveMode()) {
199       return PowerManager.LOCATION_MODE_NO_CHANGE;
200     }
201     return locationMode;
202   }
203 
204   /** Sets the value returned by {@link #getLocationPowerSaveMode()} when battery saver is on. */
setLocationPowerSaveMode(@owerManager.LocationPowerSaveMode int locationMode)205   public void setLocationPowerSaveMode(@PowerManager.LocationPowerSaveMode int locationMode) {
206     checkState(
207         locationMode >= PowerManager.MIN_LOCATION_MODE,
208         "Location Power Save Mode must be at least " + PowerManager.MIN_LOCATION_MODE);
209     checkState(
210         locationMode <= PowerManager.MAX_LOCATION_MODE,
211         "Location Power Save Mode must be no more than " + PowerManager.MAX_LOCATION_MODE);
212     this.locationMode = locationMode;
213   }
214 
215   /** This function returns the current thermal status of the device. */
216   @Implementation(minSdk = Q)
getCurrentThermalStatus()217   protected int getCurrentThermalStatus() {
218     return thermalStatus;
219   }
220 
221   /** This function adds a listener for thermal status change. */
222   @Implementation(minSdk = Q)
addThermalStatusListener( @lassName"android.os.PowerManager$OnThermalStatusChangedListener") Object listener)223   protected void addThermalStatusListener(
224       @ClassName("android.os.PowerManager$OnThermalStatusChangedListener") Object listener) {
225     checkState(
226         listener instanceof PowerManager.OnThermalStatusChangedListener,
227         "Listener must implement PowerManager.OnThermalStatusChangedListener");
228     this.thermalListeners.add(listener);
229   }
230 
231   /** This function gets listeners for thermal status change. */
getThermalStatusListeners()232   public ImmutableSet<Object> getThermalStatusListeners() {
233     return ImmutableSet.copyOf(this.thermalListeners);
234   }
235 
236   /** This function removes a listener for thermal status change. */
237   @Implementation(minSdk = Q)
removeThermalStatusListener( @lassName"android.os.PowerManager$OnThermalStatusChangedListener") Object listener)238   protected void removeThermalStatusListener(
239       @ClassName("android.os.PowerManager$OnThermalStatusChangedListener") Object listener) {
240     checkState(
241         listener instanceof PowerManager.OnThermalStatusChangedListener,
242         "Listener must implement PowerManager.OnThermalStatusChangedListener");
243     this.thermalListeners.remove(listener);
244   }
245 
246   /** Sets the value returned by {@link #getCurrentThermalStatus()}. */
setCurrentThermalStatus(int thermalStatus)247   public void setCurrentThermalStatus(int thermalStatus) {
248     checkState(
249         thermalStatus >= PowerManager.THERMAL_STATUS_NONE,
250         "Thermal status must be at least " + PowerManager.THERMAL_STATUS_NONE);
251     checkState(
252         thermalStatus <= PowerManager.THERMAL_STATUS_SHUTDOWN,
253         "Thermal status must be no more than " + PowerManager.THERMAL_STATUS_SHUTDOWN);
254     this.thermalStatus = thermalStatus;
255     for (Object listener : thermalListeners) {
256       ((PowerManager.OnThermalStatusChangedListener) listener)
257           .onThermalStatusChanged(thermalStatus);
258     }
259   }
260 
261   /** Discards the most recent {@code PowerManager.WakeLock}s */
262   @Resetter
reset()263   public static void reset() {
264     isInteractive = true;
265     isPowerSaveMode = false;
266     isDeviceIdleMode = false;
267     isLightDeviceIdleMode = false;
268     batteryDischargePrediction = null;
269     isBatteryDischargePredictionPersonalized = false;
270     locationMode = PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF;
271     rebootReasons.clear();
272     ignoringBatteryOptimizations.clear();
273     thermalStatus = 0;
274     thermalListeners.clear();
275     ambientDisplaySuppressionTokens.clear();
276     isAmbientDisplayAvailable = true;
277     isRebootingUserspaceSupported = false;
278     adaptivePowerSaveEnabled = false;
279     latestWakeLock = null;
280     lowPowerStandbyEnabled = false;
281     lowPowerStandbySupported = false;
282     exemptFromLowPowerStandby = false;
283     allowedFeatures.clear();
284     clearWakeLocks();
285   }
286 
287   /**
288    * Retrieves the most recent wakelock registered by the application
289    *
290    * @return Most recent wake lock.
291    */
getLatestWakeLock()292   public static PowerManager.WakeLock getLatestWakeLock() {
293     return latestWakeLock;
294   }
295 
296   /** Clears most recent recorded wakelock. */
clearWakeLocks()297   public static void clearWakeLocks() {
298     latestWakeLock = null;
299   }
300 
301   /**
302    * Controls result from {@link #getLatestWakeLock()}
303    *
304    * @deprecated do not use
305    */
306   @Deprecated
addWakeLock(WakeLock wl)307   static void addWakeLock(WakeLock wl) {
308     latestWakeLock = wl;
309   }
310 
311   @Implementation(minSdk = M)
isIgnoringBatteryOptimizations(String packageName)312   protected boolean isIgnoringBatteryOptimizations(String packageName) {
313     Boolean result = ignoringBatteryOptimizations.get(packageName);
314     return result == null ? false : result;
315   }
316 
setIgnoringBatteryOptimizations(String packageName, boolean value)317   public void setIgnoringBatteryOptimizations(String packageName, boolean value) {
318     ignoringBatteryOptimizations.put(packageName, Boolean.valueOf(value));
319   }
320 
321   /**
322    * Differs from real implementation as device charging state is not checked.
323    *
324    * @param timeRemaining The time remaining as a {@link Duration}.
325    * @param isPersonalized true if personalized based on device usage history, false otherwise.
326    */
327   @SystemApi
328   @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
329   @Implementation(minSdk = S)
setBatteryDischargePrediction( @onNull Duration timeRemaining, boolean isPersonalized)330   protected void setBatteryDischargePrediction(
331       @NonNull Duration timeRemaining, boolean isPersonalized) {
332     this.batteryDischargePrediction = timeRemaining;
333     this.isBatteryDischargePredictionPersonalized = isPersonalized;
334   }
335 
336   /**
337    * Returns the current battery life remaining estimate.
338    *
339    * <p>Differs from real implementation as the time that {@link #setBatteryDischargePrediction} was
340    * called is not taken into account.
341    *
342    * @return The estimated battery life remaining as a {@link Duration}. Will be {@code null} if the
343    *     prediction has not been set.
344    */
345   @Nullable
346   @Implementation(minSdk = S)
getBatteryDischargePrediction()347   protected Duration getBatteryDischargePrediction() {
348     return this.batteryDischargePrediction;
349   }
350 
351   /**
352    * Returns whether the current battery life remaining estimate is personalized based on device
353    * usage history or not. This value does not take a device's powered or charging state into
354    * account.
355    *
356    * @return A boolean indicating if the current discharge estimate is personalized based on
357    *     historical device usage or not.
358    */
359   @Implementation(minSdk = S)
isBatteryDischargePredictionPersonalized()360   protected boolean isBatteryDischargePredictionPersonalized() {
361     return this.isBatteryDischargePredictionPersonalized;
362   }
363 
364   @Implementation
reboot(@ullable String reason)365   protected void reboot(@Nullable String reason) {
366     if (RuntimeEnvironment.getApiLevel() >= R
367         && "userspace".equals(reason)
368         && !isRebootingUserspaceSupported()) {
369       throw new UnsupportedOperationException(
370           "Attempted userspace reboot on a device that doesn't support it");
371     }
372     rebootReasons.add(reason);
373   }
374 
375   /** Returns the number of times {@link #reboot(String)} was called. */
getTimesRebooted()376   public int getTimesRebooted() {
377     return rebootReasons.size();
378   }
379 
380   /**
381    * Returns the list of reasons for each reboot, in chronological order. May contain {@code null}.
382    */
getRebootReasons()383   public List<String> getRebootReasons() {
384     return new ArrayList<>(rebootReasons);
385   }
386 
387   /** Sets the value returned by {@link #isAmbientDisplayAvailable()}. */
setAmbientDisplayAvailable(boolean available)388   public void setAmbientDisplayAvailable(boolean available) {
389     this.isAmbientDisplayAvailable = available;
390   }
391 
392   /** Sets the value returned by {@link #isRebootingUserspaceSupported()}. */
setIsRebootingUserspaceSupported(boolean supported)393   public void setIsRebootingUserspaceSupported(boolean supported) {
394     this.isRebootingUserspaceSupported = supported;
395   }
396 
397   /**
398    * Returns true by default, or the value specified via {@link
399    * #setAmbientDisplayAvailable(boolean)}.
400    */
401   @Implementation(minSdk = R)
isAmbientDisplayAvailable()402   protected boolean isAmbientDisplayAvailable() {
403     return isAmbientDisplayAvailable;
404   }
405 
406   /**
407    * If true, suppress the device's ambient display. Ambient display is defined as anything visible
408    * on the display when {@link PowerManager#isInteractive} is false.
409    *
410    * @param token An identifier for the ambient display suppression.
411    * @param suppress If {@code true}, suppresses the ambient display. Otherwise, unsuppresses the
412    *     ambient display for the given token.
413    */
414   @Implementation(minSdk = R)
suppressAmbientDisplay(String token, boolean suppress)415   protected void suppressAmbientDisplay(String token, boolean suppress) {
416     String suppressionToken = Binder.getCallingUid() + "_" + token;
417     if (suppress) {
418       ambientDisplaySuppressionTokens.add(suppressionToken);
419     } else {
420       ambientDisplaySuppressionTokens.remove(suppressionToken);
421     }
422   }
423 
424   /**
425    * Returns true if {@link #suppressAmbientDisplay(String, boolean)} has been called with any
426    * token.
427    */
428   @Implementation(minSdk = R)
isAmbientDisplaySuppressed()429   protected boolean isAmbientDisplaySuppressed() {
430     return !ambientDisplaySuppressionTokens.isEmpty();
431   }
432 
433   /**
434    * Returns last value specified in {@link #setIsRebootingUserspaceSupported(boolean)} or {@code
435    * false} by default.
436    */
437   @Implementation(minSdk = R)
isRebootingUserspaceSupported()438   protected boolean isRebootingUserspaceSupported() {
439     return isRebootingUserspaceSupported;
440   }
441 
442   /**
443    * Sets whether Adaptive Power Saver is enabled.
444    *
445    * <p>This has no effect, other than the value of {@link #getAdaptivePowerSaveEnabled()} is
446    * changed, which can be used to ensure this method is called correctly.
447    *
448    * @return true if the value has changed.
449    */
450   @Implementation(minSdk = Q)
451   @SystemApi
setAdaptivePowerSaveEnabled(boolean enabled)452   protected boolean setAdaptivePowerSaveEnabled(boolean enabled) {
453     boolean changed = adaptivePowerSaveEnabled != enabled;
454     adaptivePowerSaveEnabled = enabled;
455     return changed;
456   }
457 
458   /** Gets the value set by {@link #setAdaptivePowerSaveEnabled(boolean)}. */
getAdaptivePowerSaveEnabled()459   public boolean getAdaptivePowerSaveEnabled() {
460     return adaptivePowerSaveEnabled;
461   }
462 
463   @Implements(PowerManager.WakeLock.class)
464   public static class ShadowWakeLock {
465     @RealObject private PowerManager.WakeLock realWakeLock;
466 
467     private boolean refCounted = true;
468     private WorkSource workSource = null;
469     private int timesHeld = 0;
470     private List<Optional<Long>> timeoutTimestampList = new ArrayList<>();
471 
acquireInternal(Optional<Long> timeoutOptional)472     private void acquireInternal(Optional<Long> timeoutOptional) {
473       ++timesHeld;
474       timeoutTimestampList.add(timeoutOptional);
475     }
476 
477     /** Iterate all the wake lock and remove those timeouted ones. */
refreshTimeoutTimestampList()478     private void refreshTimeoutTimestampList() {
479       timeoutTimestampList =
480           timeoutTimestampList.stream()
481               .filter(o -> !o.isPresent() || o.get() >= SystemClock.elapsedRealtime())
482               .collect(toCollection(ArrayList::new));
483     }
484 
485     @Implementation
acquire()486     protected void acquire() {
487       acquireInternal(Optional.empty());
488     }
489 
490     @Implementation
acquire(long timeout)491     protected synchronized void acquire(long timeout) {
492       Long timeoutMillis = timeout + SystemClock.elapsedRealtime();
493       if (timeoutMillis > 0) {
494         acquireInternal(Optional.of(timeoutMillis));
495       } else {
496         // This is because many existing tests use Long.MAX_VALUE as timeout, which will cause a
497         // long overflow.
498         acquireInternal(Optional.empty());
499       }
500     }
501 
502     /** Releases the wake lock. The {@code flags} are ignored. */
503     @Implementation
release(int flags)504     protected synchronized void release(int flags) {
505       refreshTimeoutTimestampList();
506 
507       // Dequeue the wake lock with smallest timeout.
508       // Map the subtracted value to 1 and -1 to avoid long->int cast overflow.
509       Optional<Optional<Long>> wakeLockOptional =
510           timeoutTimestampList.stream()
511               .min(
512                   comparing(
513                       (Optional<Long> arg) -> arg.orElse(Long.MAX_VALUE),
514                       (Long leftProperty, Long rightProperty) ->
515                           (leftProperty - rightProperty) > 0 ? 1 : -1));
516 
517       if (wakeLockOptional.isEmpty()) {
518         if (refCounted) {
519           throw new RuntimeException("WakeLock under-locked");
520         } else {
521           return;
522         }
523       }
524 
525       Optional<Long> wakeLock = wakeLockOptional.get();
526 
527       if (refCounted) {
528         timeoutTimestampList.remove(wakeLock);
529       } else {
530         // If a wake lock is not reference counted, then one call to release() is sufficient to undo
531         // the effect of all previous calls to acquire().
532         timeoutTimestampList = new ArrayList<>();
533       }
534     }
535 
536     @Implementation
isHeld()537     protected synchronized boolean isHeld() {
538       refreshTimeoutTimestampList();
539       return !timeoutTimestampList.isEmpty();
540     }
541 
542     /**
543      * Retrieves if the wake lock is reference counted or not
544      *
545      * @return Is the wake lock reference counted?
546      */
isReferenceCounted()547     public boolean isReferenceCounted() {
548       return refCounted;
549     }
550 
551     @Implementation
setReferenceCounted(boolean value)552     protected void setReferenceCounted(boolean value) {
553       refCounted = value;
554     }
555 
556     @Implementation
setWorkSource(WorkSource ws)557     protected synchronized void setWorkSource(WorkSource ws) {
558       workSource = ws;
559     }
560 
getWorkSource()561     public synchronized WorkSource getWorkSource() {
562       return workSource;
563     }
564 
565     /** Returns how many times the wakelock was held. */
getTimesHeld()566     public int getTimesHeld() {
567       return timesHeld;
568     }
569 
570     /** Returns the tag. */
571     @HiddenApi
572     @Implementation(minSdk = O)
getTag()573     public String getTag() {
574       return reflector(WakeLockReflector.class, realWakeLock).getTag();
575     }
576 
577     @ForType(PowerManager.WakeLock.class)
578     private interface WakeLockReflector {
579       @Accessor("mTag")
getTag()580       String getTag();
581     }
582   }
583 
getContext()584   private Context getContext() {
585     return reflector(PowerManagerReflector.class, realPowerManager).getContext();
586   }
587 
588   @Implementation(minSdk = TIRAMISU)
isLowPowerStandbySupported()589   protected boolean isLowPowerStandbySupported() {
590     return lowPowerStandbySupported;
591   }
592 
593   @TargetApi(TIRAMISU)
setLowPowerStandbySupported(boolean lowPowerStandbySupported)594   public void setLowPowerStandbySupported(boolean lowPowerStandbySupported) {
595     this.lowPowerStandbySupported = lowPowerStandbySupported;
596   }
597 
598   @Implementation(minSdk = TIRAMISU)
isLowPowerStandbyEnabled()599   protected boolean isLowPowerStandbyEnabled() {
600     return lowPowerStandbySupported && lowPowerStandbyEnabled;
601   }
602 
603   @Implementation(minSdk = TIRAMISU)
setLowPowerStandbyEnabled(boolean lowPowerStandbyEnabled)604   public void setLowPowerStandbyEnabled(boolean lowPowerStandbyEnabled) {
605     this.lowPowerStandbyEnabled = lowPowerStandbyEnabled;
606   }
607 
608   @Implementation(minSdk = UPSIDE_DOWN_CAKE)
isAllowedInLowPowerStandby(String feature)609   protected boolean isAllowedInLowPowerStandby(String feature) {
610     if (!lowPowerStandbySupported) {
611       return true;
612     }
613     return allowedFeatures.contains(feature);
614   }
615 
616   @TargetApi(UPSIDE_DOWN_CAKE)
addAllowedInLowPowerStandby(String feature)617   public void addAllowedInLowPowerStandby(String feature) {
618     allowedFeatures.add(feature);
619   }
620 
621   @Implementation(minSdk = UPSIDE_DOWN_CAKE)
isExemptFromLowPowerStandby()622   protected boolean isExemptFromLowPowerStandby() {
623     if (!lowPowerStandbySupported) {
624       return true;
625     }
626     return exemptFromLowPowerStandby;
627   }
628 
629   @TargetApi(UPSIDE_DOWN_CAKE)
setExemptFromLowPowerStandby(boolean exemptFromLowPowerStandby)630   public void setExemptFromLowPowerStandby(boolean exemptFromLowPowerStandby) {
631     this.exemptFromLowPowerStandby = exemptFromLowPowerStandby;
632   }
633 
634   @Implementation(minSdk = UPSIDE_DOWN_CAKE)
635   protected @ClassName("android.os.PowerManager$LowPowerStandbyPortsLock") Object
newLowPowerStandbyPortsLock(List<LowPowerStandbyPortDescription> ports)636       newLowPowerStandbyPortsLock(List<LowPowerStandbyPortDescription> ports) {
637     PowerManager.LowPowerStandbyPortsLock lock =
638         Shadow.newInstanceOf(PowerManager.LowPowerStandbyPortsLock.class);
639     ((ShadowLowPowerStandbyPortsLock) Shadow.extract(lock)).setPorts(ports);
640     return lock;
641   }
642 
643   /** Shadow of {@link LowPowerStandbyPortsLock} to allow testing state. */
644   @Implements(
645       value = PowerManager.LowPowerStandbyPortsLock.class,
646       minSdk = UPSIDE_DOWN_CAKE,
647       isInAndroidSdk = false)
648   public static class ShadowLowPowerStandbyPortsLock {
649     private List<LowPowerStandbyPortDescription> ports;
650     private boolean isAcquired = false;
651     private int acquireCount = 0;
652 
653     @Implementation(minSdk = UPSIDE_DOWN_CAKE)
acquire()654     protected void acquire() {
655       isAcquired = true;
656       acquireCount++;
657     }
658 
659     @Implementation(minSdk = UPSIDE_DOWN_CAKE)
release()660     protected void release() {
661       isAcquired = false;
662     }
663 
isAcquired()664     public boolean isAcquired() {
665       return isAcquired;
666     }
667 
getAcquireCount()668     public int getAcquireCount() {
669       return acquireCount;
670     }
671 
setPorts(List<LowPowerStandbyPortDescription> ports)672     public void setPorts(List<LowPowerStandbyPortDescription> ports) {
673       this.ports = ports;
674     }
675 
getPorts()676     public List<LowPowerStandbyPortDescription> getPorts() {
677       return ports;
678     }
679   }
680 
681   /** Reflector interface for {@link PowerManager}'s internals. */
682   @ForType(PowerManager.class)
683   private interface PowerManagerReflector {
684 
685     @Accessor("mContext")
getContext()686     Context getContext();
687 
688     @Direct
newWakeLock(int flags, String tag)689     WakeLock newWakeLock(int flags, String tag);
690   }
691 }
692