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