• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.TIRAMISU;
4 import static com.google.common.base.Preconditions.checkArgument;
5 import static org.robolectric.util.reflector.Reflector.reflector;
6 
7 import android.annotation.SystemApi;
8 import android.app.IUiModeManager;
9 import android.app.UiModeManager;
10 import android.content.ContentResolver;
11 import android.content.Context;
12 import android.content.pm.PackageManager;
13 import android.content.res.Configuration;
14 import android.os.Build.VERSION;
15 import android.os.Build.VERSION_CODES;
16 import android.os.IBinder;
17 import android.provider.Settings;
18 import com.android.internal.annotations.GuardedBy;
19 import com.google.common.collect.ImmutableSet;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Set;
24 import org.robolectric.RuntimeEnvironment;
25 import org.robolectric.annotation.HiddenApi;
26 import org.robolectric.annotation.Implementation;
27 import org.robolectric.annotation.Implements;
28 import org.robolectric.annotation.RealObject;
29 import org.robolectric.annotation.Resetter;
30 import org.robolectric.util.reflector.Accessor;
31 import org.robolectric.util.reflector.Constructor;
32 import org.robolectric.util.reflector.ForType;
33 import org.robolectric.util.reflector.Static;
34 import org.robolectric.util.reflector.WithType;
35 import org.robolectric.versioning.AndroidVersions.V;
36 
37 /** Shadow for {@link UiModeManager}. */
38 @Implements(UiModeManager.class)
39 public class ShadowUIModeManager {
40   private static final int DEFAULT_PRIORITY = 0;
41 
42   public int currentModeType = Configuration.UI_MODE_TYPE_UNDEFINED;
43   public int currentNightMode = UiModeManager.MODE_NIGHT_AUTO;
44   public int lastFlags;
45   public int lastCarModePriority;
46   private int currentApplicationNightMode = 0;
47   private final Map<Integer, Set<String>> activeProjectionTypes = new HashMap<>();
48   private boolean failOnProjectionToggle;
49   private final Object lock = new Object();
50 
51   @GuardedBy("lock")
52   private int nightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
53 
54   @GuardedBy("lock")
55   private boolean isNightModeOn = false;
56 
57   @RealObject UiModeManager realUiModeManager;
58 
59   private static final ImmutableSet<Integer> VALID_NIGHT_MODE_CUSTOM_TYPES =
60       ImmutableSet.of(
61           UiModeManager.MODE_NIGHT_CUSTOM_TYPE_SCHEDULE,
62           UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
63 
64   @Implementation
getCurrentModeType()65   protected int getCurrentModeType() {
66     return currentModeType;
67   }
68 
setCurrentModeType(int modeType)69   public void setCurrentModeType(int modeType) {
70     this.currentModeType = modeType;
71   }
72 
73   @Implementation(maxSdk = VERSION_CODES.Q)
enableCarMode(int flags)74   protected void enableCarMode(int flags) {
75     enableCarMode(DEFAULT_PRIORITY, flags);
76   }
77 
78   @Implementation(minSdk = VERSION_CODES.R)
enableCarMode(int priority, int flags)79   protected void enableCarMode(int priority, int flags) {
80     currentModeType = Configuration.UI_MODE_TYPE_CAR;
81     lastCarModePriority = priority;
82     lastFlags = flags;
83   }
84 
85   @Implementation
disableCarMode(int flags)86   protected void disableCarMode(int flags) {
87     currentModeType = Configuration.UI_MODE_TYPE_NORMAL;
88     lastFlags = flags;
89   }
90 
91   @Implementation
getNightMode()92   protected int getNightMode() {
93     return currentNightMode;
94   }
95 
96   @Implementation
setNightMode(int mode)97   protected void setNightMode(int mode) {
98     synchronized (lock) {
99       ContentResolver resolver = getContentResolver();
100       switch (mode) {
101         case UiModeManager.MODE_NIGHT_NO:
102         case UiModeManager.MODE_NIGHT_YES:
103         case UiModeManager.MODE_NIGHT_AUTO:
104           currentNightMode = mode;
105           nightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
106           if (resolver != null) {
107             Settings.Secure.putInt(resolver, Settings.Secure.UI_NIGHT_MODE, mode);
108             Settings.Secure.putInt(
109                 resolver,
110                 Settings.Secure.UI_NIGHT_MODE_CUSTOM_TYPE,
111                 UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
112           }
113           break;
114         default:
115           currentNightMode = UiModeManager.MODE_NIGHT_AUTO;
116           if (resolver != null) {
117             Settings.Secure.putInt(
118                 resolver, Settings.Secure.UI_NIGHT_MODE, UiModeManager.MODE_NIGHT_AUTO);
119           }
120       }
121     }
122   }
123 
124   @Implementation(minSdk = VERSION_CODES.S)
getProjectingPackages(int projectionType)125   protected Set<String> getProjectingPackages(int projectionType) {
126     if (projectionType == UiModeManager.PROJECTION_TYPE_ALL) {
127       Set<String> projections = new HashSet<>();
128       activeProjectionTypes.values().forEach(projections::addAll);
129       return projections;
130     }
131     return activeProjectionTypes.getOrDefault(projectionType, new HashSet<>());
132   }
133 
getApplicationNightMode()134   public int getApplicationNightMode() {
135     return currentApplicationNightMode;
136   }
137 
getActiveProjectionTypes()138   public Set<Integer> getActiveProjectionTypes() {
139     return new HashSet<>(activeProjectionTypes.keySet());
140   }
141 
setFailOnProjectionToggle(boolean failOnProjectionToggle)142   public void setFailOnProjectionToggle(boolean failOnProjectionToggle) {
143     this.failOnProjectionToggle = failOnProjectionToggle;
144   }
145 
146   @Implementation(minSdk = VERSION_CODES.S)
147   @HiddenApi
setApplicationNightMode(int mode)148   protected void setApplicationNightMode(int mode) {
149     currentApplicationNightMode = mode;
150   }
151 
152   @Implementation(minSdk = VERSION_CODES.S)
153   @SystemApi
requestProjection(int projectionType)154   protected boolean requestProjection(int projectionType) {
155     if (projectionType == UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) {
156       assertHasPermission(android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
157     }
158     if (failOnProjectionToggle) {
159       return false;
160     }
161     Set<String> projections = activeProjectionTypes.getOrDefault(projectionType, new HashSet<>());
162     projections.add(RuntimeEnvironment.getApplication().getPackageName());
163     activeProjectionTypes.put(projectionType, projections);
164 
165     return true;
166   }
167 
168   @Implementation(minSdk = VERSION_CODES.S)
169   @SystemApi
releaseProjection(int projectionType)170   protected boolean releaseProjection(int projectionType) {
171     if (projectionType == UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) {
172       assertHasPermission(android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
173     }
174     if (failOnProjectionToggle) {
175       return false;
176     }
177     String packageName = RuntimeEnvironment.getApplication().getPackageName();
178     Set<String> projections = activeProjectionTypes.getOrDefault(projectionType, new HashSet<>());
179     if (projections.contains(packageName)) {
180       projections.remove(packageName);
181       if (projections.isEmpty()) {
182         activeProjectionTypes.remove(projectionType);
183       } else {
184         activeProjectionTypes.put(projectionType, projections);
185       }
186       return true;
187     }
188 
189     return false;
190   }
191 
192   @Implementation(minSdk = TIRAMISU)
getNightModeCustomType()193   protected int getNightModeCustomType() {
194     synchronized (lock) {
195       return nightModeCustomType;
196     }
197   }
198 
199   /** Returns whether night mode is currently on when a custom night mode type is selected. */
isNightModeOn()200   public boolean isNightModeOn() {
201     synchronized (lock) {
202       return isNightModeOn;
203     }
204   }
205 
206   @Implementation(minSdk = TIRAMISU)
setNightModeCustomType(int mode)207   protected void setNightModeCustomType(int mode) {
208     synchronized (lock) {
209       ContentResolver resolver = getContentResolver();
210       if (VALID_NIGHT_MODE_CUSTOM_TYPES.contains(mode)) {
211         nightModeCustomType = mode;
212         currentNightMode = UiModeManager.MODE_NIGHT_CUSTOM;
213         if (resolver != null) {
214           Settings.Secure.putInt(resolver, Settings.Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mode);
215         }
216       } else {
217         nightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
218         if (resolver != null) {
219           Settings.Secure.putInt(
220               resolver,
221               Settings.Secure.UI_NIGHT_MODE_CUSTOM_TYPE,
222               UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
223         }
224       }
225     }
226   }
227 
getContentResolver()228   private ContentResolver getContentResolver() {
229     Context context = getContext();
230     return context == null ? null : context.getContentResolver();
231   }
232 
233   // Note: UiModeManager stores the context only starting from Android R.
getContext()234   private Context getContext() {
235     if (VERSION.SDK_INT < VERSION_CODES.R) {
236       return null;
237     }
238     return reflector(UiModeManagerReflector.class, realUiModeManager).getContext();
239   }
240 
241   @Implementation(minSdk = TIRAMISU)
setNightModeActivatedForCustomMode(int mode, boolean active)242   protected boolean setNightModeActivatedForCustomMode(int mode, boolean active) {
243     synchronized (lock) {
244       if (VALID_NIGHT_MODE_CUSTOM_TYPES.contains(mode) && nightModeCustomType == mode) {
245         isNightModeOn = active;
246         return true;
247       }
248       return false;
249     }
250   }
251 
252   /**
253    * Sets the contrast value.
254    *
255    * <p>The default value for contrast is 0.0f. The permitted values are between -1.0f and 1.0f
256    * inclusive.
257    */
setContrast(float contrast)258   public void setContrast(float contrast) {
259     checkArgument(
260         contrast >= -1.0f && contrast <= 1.0f,
261         "Contrast value must be between -1.0f and 1.0f inclusive. Provided value: %s",
262         contrast);
263 
264     if (RuntimeEnvironment.getApiLevel() == VERSION_CODES.UPSIDE_DOWN_CAKE) {
265       reflector(UiModeManagerReflector.class, realUiModeManager).setContrast(contrast);
266     } else {
267       Object globals = reflector(UiModeManagerReflector.class, realUiModeManager).getGlobals();
268       reflector(UiModeManagerGlobalsReflector.class, globals).setContrast(contrast);
269     }
270   }
271 
272   @Resetter
reset()273   public static void reset() {
274     if (RuntimeEnvironment.getApiLevel() >= V.SDK_INT) {
275       IUiModeManager service =
276           IUiModeManager.Stub.asInterface(
277               reflector(ServiceManagerReflector.class).getServiceOrThrow(Context.UI_MODE_SERVICE));
278       reflector(UiModeManagerReflector.class)
279           .setGlobals(reflector(UiModeManagerGlobalsReflector.class).newGlobals(service));
280     }
281   }
282 
283   @ForType(className = "android.os.ServiceManager")
284   interface ServiceManagerReflector {
285     @Static
getServiceOrThrow(String name)286     IBinder getServiceOrThrow(String name);
287   }
288 
289   @ForType(UiModeManager.class)
290   interface UiModeManagerReflector {
291     @Accessor("mContext")
getContext()292     Context getContext();
293 
294     @Accessor("mContrast")
setContrast(float value)295     void setContrast(float value); // Stores the contrast value for Android U.
296 
297     @Accessor("sGlobals")
298     @Static
getGlobals()299     Object getGlobals(); // Stores the contrast value for Android V and above.
300 
301     @Accessor("sGlobals")
302     @Static
setGlobals(@ithType"android.app.UiModeManager$Globals") Object value)303     void setGlobals(@WithType("android.app.UiModeManager$Globals") Object value);
304   }
305 
306   @ForType(className = "android.app.UiModeManager$Globals")
307   interface UiModeManagerGlobalsReflector {
308     @Accessor("mContrast")
setContrast(float contrast)309     void setContrast(float contrast);
310 
311     @Constructor
newGlobals(IUiModeManager iUiModeManager)312     Object newGlobals(IUiModeManager iUiModeManager);
313   }
314 
assertHasPermission(String... permissions)315   private void assertHasPermission(String... permissions) {
316     Context context = RuntimeEnvironment.getApplication();
317     for (String permission : permissions) {
318       // Check both the Runtime based and Manifest based permissions
319       if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED
320           && context.getPackageManager().checkPermission(permission, context.getPackageName())
321               != PackageManager.PERMISSION_GRANTED) {
322         throw new SecurityException("Missing required permission: " + permission);
323       }
324     }
325   }
326 }
327