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