1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.TIRAMISU;
4 import static org.robolectric.util.reflector.Reflector.reflector;
5 
6 import android.annotation.SystemApi;
7 import android.app.UiModeManager;
8 import android.content.ContentResolver;
9 import android.content.Context;
10 import android.content.pm.PackageManager;
11 import android.content.res.Configuration;
12 import android.os.Build.VERSION;
13 import android.os.Build.VERSION_CODES;
14 import android.provider.Settings;
15 import com.android.internal.annotations.GuardedBy;
16 import com.google.common.collect.ImmutableSet;
17 import java.util.HashMap;
18 import java.util.HashSet;
19 import java.util.Map;
20 import java.util.Set;
21 import org.robolectric.RuntimeEnvironment;
22 import org.robolectric.annotation.HiddenApi;
23 import org.robolectric.annotation.Implementation;
24 import org.robolectric.annotation.Implements;
25 import org.robolectric.annotation.RealObject;
26 import org.robolectric.util.reflector.Accessor;
27 import org.robolectric.util.reflector.ForType;
28 
29 /** Shadow for {@link UiModeManager}. */
30 @Implements(UiModeManager.class)
31 public class ShadowUIModeManager {
32   public int currentModeType = Configuration.UI_MODE_TYPE_UNDEFINED;
33   public int currentNightMode = UiModeManager.MODE_NIGHT_AUTO;
34   public int lastFlags;
35   public int lastCarModePriority;
36   private int currentApplicationNightMode = 0;
37   private final Map<Integer, Set<String>> activeProjectionTypes = new HashMap<>();
38   private boolean failOnProjectionToggle;
39 
40   private static final ImmutableSet<Integer> VALID_NIGHT_MODES =
41       ImmutableSet.of(
42           UiModeManager.MODE_NIGHT_AUTO, UiModeManager.MODE_NIGHT_NO, UiModeManager.MODE_NIGHT_YES);
43 
44   private static final int DEFAULT_PRIORITY = 0;
45 
46   private final Object lock = new Object();
47 
48   @GuardedBy("lock")
49   private int nightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
50 
51   @GuardedBy("lock")
52   private boolean isNightModeOn = false;
53 
54   @RealObject UiModeManager realUiModeManager;
55 
56   private static final ImmutableSet<Integer> VALID_NIGHT_MODE_CUSTOM_TYPES =
57       ImmutableSet.of(
58           UiModeManager.MODE_NIGHT_CUSTOM_TYPE_SCHEDULE,
59           UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
60 
61   @Implementation
getCurrentModeType()62   protected int getCurrentModeType() {
63     return currentModeType;
64   }
65 
setCurrentModeType(int modeType)66   public void setCurrentModeType(int modeType) {
67     this.currentModeType = modeType;
68   }
69 
70   @Implementation(maxSdk = VERSION_CODES.Q)
enableCarMode(int flags)71   protected void enableCarMode(int flags) {
72     enableCarMode(DEFAULT_PRIORITY, flags);
73   }
74 
75   @Implementation(minSdk = VERSION_CODES.R)
enableCarMode(int priority, int flags)76   protected void enableCarMode(int priority, int flags) {
77     currentModeType = Configuration.UI_MODE_TYPE_CAR;
78     lastCarModePriority = priority;
79     lastFlags = flags;
80   }
81 
82   @Implementation
disableCarMode(int flags)83   protected void disableCarMode(int flags) {
84     currentModeType = Configuration.UI_MODE_TYPE_NORMAL;
85     lastFlags = flags;
86   }
87 
88   @Implementation
getNightMode()89   protected int getNightMode() {
90     return currentNightMode;
91   }
92 
93   @Implementation
setNightMode(int mode)94   protected void setNightMode(int mode) {
95     synchronized (lock) {
96       ContentResolver resolver = getContentResolver();
97       switch (mode) {
98         case UiModeManager.MODE_NIGHT_NO:
99         case UiModeManager.MODE_NIGHT_YES:
100         case UiModeManager.MODE_NIGHT_AUTO:
101           currentNightMode = mode;
102           nightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
103           if (resolver != null) {
104             Settings.Secure.putInt(resolver, Settings.Secure.UI_NIGHT_MODE, mode);
105             Settings.Secure.putInt(
106                 resolver,
107                 Settings.Secure.UI_NIGHT_MODE_CUSTOM_TYPE,
108                 UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
109           }
110           break;
111         default:
112           currentNightMode = UiModeManager.MODE_NIGHT_AUTO;
113           if (resolver != null) {
114             Settings.Secure.putInt(
115                 resolver, Settings.Secure.UI_NIGHT_MODE, UiModeManager.MODE_NIGHT_AUTO);
116           }
117       }
118     }
119   }
120 
121   @Implementation(minSdk = VERSION_CODES.S)
getProjectingPackages(int projectionType)122   protected Set<String> getProjectingPackages(int projectionType) {
123     if (projectionType == UiModeManager.PROJECTION_TYPE_ALL) {
124       Set<String> projections = new HashSet<>();
125       activeProjectionTypes.values().forEach(projections::addAll);
126       return projections;
127     }
128     return activeProjectionTypes.getOrDefault(projectionType, new HashSet<>());
129   }
130 
getApplicationNightMode()131   public int getApplicationNightMode() {
132     return currentApplicationNightMode;
133   }
134 
getActiveProjectionTypes()135   public Set<Integer> getActiveProjectionTypes() {
136     return new HashSet<>(activeProjectionTypes.keySet());
137   }
138 
setFailOnProjectionToggle(boolean failOnProjectionToggle)139   public void setFailOnProjectionToggle(boolean failOnProjectionToggle) {
140     this.failOnProjectionToggle = failOnProjectionToggle;
141   }
142 
143   @Implementation(minSdk = VERSION_CODES.S)
144   @HiddenApi
setApplicationNightMode(int mode)145   protected void setApplicationNightMode(int mode) {
146     currentApplicationNightMode = mode;
147   }
148 
149   @Implementation(minSdk = VERSION_CODES.S)
150   @SystemApi
requestProjection(int projectionType)151   protected boolean requestProjection(int projectionType) {
152     if (projectionType == UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) {
153       assertHasPermission(android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
154     }
155     if (failOnProjectionToggle) {
156       return false;
157     }
158     Set<String> projections = activeProjectionTypes.getOrDefault(projectionType, new HashSet<>());
159     projections.add(RuntimeEnvironment.getApplication().getPackageName());
160     activeProjectionTypes.put(projectionType, projections);
161 
162     return true;
163   }
164 
165   @Implementation(minSdk = VERSION_CODES.S)
166   @SystemApi
releaseProjection(int projectionType)167   protected boolean releaseProjection(int projectionType) {
168     if (projectionType == UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) {
169       assertHasPermission(android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
170     }
171     if (failOnProjectionToggle) {
172       return false;
173     }
174     String packageName = RuntimeEnvironment.getApplication().getPackageName();
175     Set<String> projections = activeProjectionTypes.getOrDefault(projectionType, new HashSet<>());
176     if (projections.contains(packageName)) {
177       projections.remove(packageName);
178       if (projections.isEmpty()) {
179         activeProjectionTypes.remove(projectionType);
180       } else {
181         activeProjectionTypes.put(projectionType, projections);
182       }
183       return true;
184     }
185 
186     return false;
187   }
188 
189   @Implementation(minSdk = TIRAMISU)
getNightModeCustomType()190   protected int getNightModeCustomType() {
191     synchronized (lock) {
192       return nightModeCustomType;
193     }
194   }
195 
196   /** Returns whether night mode is currently on when a custom night mode type is selected. */
isNightModeOn()197   public boolean isNightModeOn() {
198     synchronized (lock) {
199       return isNightModeOn;
200     }
201   }
202 
203   @Implementation(minSdk = TIRAMISU)
setNightModeCustomType(int mode)204   protected void setNightModeCustomType(int mode) {
205     synchronized (lock) {
206       ContentResolver resolver = getContentResolver();
207       if (VALID_NIGHT_MODE_CUSTOM_TYPES.contains(mode)) {
208         nightModeCustomType = mode;
209         currentNightMode = UiModeManager.MODE_NIGHT_CUSTOM;
210         if (resolver != null) {
211           Settings.Secure.putInt(resolver, Settings.Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mode);
212         }
213       } else {
214         nightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
215         if (resolver != null) {
216           Settings.Secure.putInt(
217               resolver,
218               Settings.Secure.UI_NIGHT_MODE_CUSTOM_TYPE,
219               UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
220         }
221       }
222     }
223   }
224 
getContentResolver()225   private ContentResolver getContentResolver() {
226     Context context = getContext();
227     return context == null ? null : context.getContentResolver();
228   }
229 
230   // Note: UiModeManager stores the context only starting from Android R.
getContext()231   private Context getContext() {
232     if (VERSION.SDK_INT < VERSION_CODES.R) {
233       return null;
234     }
235     return reflector(UiModeManagerReflector.class, realUiModeManager).getContext();
236   }
237 
238   @Implementation(minSdk = TIRAMISU)
setNightModeActivatedForCustomMode(int mode, boolean active)239   protected boolean setNightModeActivatedForCustomMode(int mode, boolean active) {
240     synchronized (lock) {
241       if (VALID_NIGHT_MODE_CUSTOM_TYPES.contains(mode) && nightModeCustomType == mode) {
242         isNightModeOn = active;
243         return true;
244       }
245       return false;
246     }
247   }
248 
249   @ForType(UiModeManager.class)
250   interface UiModeManagerReflector {
251     @Accessor("mContext")
getContext()252     Context getContext();
253   }
254 
assertHasPermission(String... permissions)255   private void assertHasPermission(String... permissions) {
256     Context context = RuntimeEnvironment.getApplication();
257     for (String permission : permissions) {
258       // Check both the Runtime based and Manifest based permissions
259       if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED
260           && context.getPackageManager().checkPermission(permission, context.getPackageName())
261               != PackageManager.PERMISSION_GRANTED) {
262         throw new SecurityException("Missing required permission: " + permission);
263       }
264     }
265   }
266 }
267