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