1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN; 4 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; 5 import static android.os.Build.VERSION_CODES.LOLLIPOP; 6 import static android.os.Build.VERSION_CODES.M; 7 import static android.os.Build.VERSION_CODES.P; 8 9 import android.content.ContentResolver; 10 import android.content.Context; 11 import android.os.Build; 12 import android.provider.Settings; 13 import android.text.TextUtils; 14 import android.util.ArrayMap; 15 16 import org.robolectric.RuntimeEnvironment; 17 import org.robolectric.annotation.Implementation; 18 import org.robolectric.annotation.Implements; 19 import org.robolectric.annotation.Resetter; 20 import org.robolectric.shadow.api.Shadow; 21 import org.robolectric.util.ReflectionHelpers.ClassParameter; 22 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.HashMap; 26 import java.util.HashSet; 27 import java.util.List; 28 import java.util.Map; 29 import java.util.Set; 30 import java.util.WeakHashMap; 31 import java.util.stream.Collectors; 32 33 @SuppressWarnings({"UnusedDeclaration"}) 34 @Implements(Settings.class) 35 public class ShadowSettings { 36 @Implements(value = Settings.System.class) 37 public static class ShadowSystem { 38 private static final Map<ContentResolver, Map<String, String>> dataMap = new WeakHashMap<>(); 39 40 @Resetter reset()41 public static void reset() { 42 dataMap.clear(); 43 } 44 45 @Implementation(minSdk = JELLY_BEAN_MR1) putStringForUser(ContentResolver cr, String name, String value, int userHandle)46 protected static boolean putStringForUser(ContentResolver cr, String name, String value, 47 int userHandle) { 48 return putString(cr, name, value); 49 } 50 51 @Implementation(minSdk = JELLY_BEAN_MR1) getStringForUser(ContentResolver cr, String name, int userHandle)52 protected static String getStringForUser(ContentResolver cr, String name, int userHandle) { 53 return getString(cr, name); 54 } 55 56 @Implementation putString(ContentResolver cr, String name, String value)57 protected static boolean putString(ContentResolver cr, String name, String value) { 58 get(cr).put(name, value); 59 return true; 60 } 61 62 @Implementation getString(ContentResolver cr, String name)63 protected static String getString(ContentResolver cr, String name) { 64 return get(cr).get(name); 65 } 66 get(ContentResolver cr)67 private static Map<String, String> get(ContentResolver cr) { 68 Map<String, String> map = dataMap.get(cr); 69 if (map == null) { 70 map = new HashMap<>(); 71 dataMap.put(cr, map); 72 } 73 return map; 74 } 75 } 76 77 @Implements(value = Settings.Secure.class) 78 public static class ShadowSecure { 79 private static final Map<ContentResolver, Map<String, String>> dataMap = new WeakHashMap<>(); 80 81 @Resetter reset()82 public static void reset() { 83 dataMap.clear(); 84 } 85 86 @Implementation(minSdk = JELLY_BEAN_MR1) putStringForUser(ContentResolver cr, String name, String value, int userHandle)87 protected static boolean putStringForUser(ContentResolver cr, String name, String value, 88 int userHandle) { 89 return putString(cr, name, value); 90 } 91 92 @Implementation(minSdk = JELLY_BEAN_MR1) getStringForUser(ContentResolver cr, String name, int userHandle)93 protected static String getStringForUser(ContentResolver cr, String name, int userHandle) { 94 return getString(cr, name); 95 } 96 97 @Implementation putString(ContentResolver cr, String name, String value)98 protected static boolean putString(ContentResolver cr, String name, String value) { 99 get(cr).put(name, value); 100 return true; 101 } 102 103 @Implementation getString(ContentResolver cr, String name)104 protected static String getString(ContentResolver cr, String name) { 105 return get(cr).get(name); 106 } 107 get(ContentResolver cr)108 private static Map<String, String> get(ContentResolver cr) { 109 Map<String, String> map = dataMap.get(cr); 110 if (map == null) { 111 map = new HashMap<>(); 112 dataMap.put(cr, map); 113 } 114 return map; 115 } 116 117 @Implementation(minSdk = JELLY_BEAN_MR1) setLocationProviderEnabledForUser( ContentResolver cr, String provider, boolean enabled, int uid)118 protected static boolean setLocationProviderEnabledForUser( 119 ContentResolver cr, String provider, boolean enabled, int uid) { 120 return updateEnabledProviders(cr, provider, enabled); 121 } 122 123 @Implementation(maxSdk = JELLY_BEAN) setLocationProviderEnabled( ContentResolver cr, String provider, boolean enabled)124 protected static void setLocationProviderEnabled( 125 ContentResolver cr, String provider, boolean enabled) { 126 updateEnabledProviders(cr, provider, enabled); 127 } 128 updateEnabledProviders( ContentResolver cr, String provider, boolean enabled)129 private static boolean updateEnabledProviders( 130 ContentResolver cr, String provider, boolean enabled) { 131 Set<String> providers = new HashSet<>(); 132 String oldProviders = 133 Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED); 134 if (!TextUtils.isEmpty(oldProviders)) { 135 providers.addAll(Arrays.asList(oldProviders.split(","))); 136 } 137 138 if (enabled) { 139 providers.add(provider); 140 } else { 141 providers.remove(provider); 142 } 143 144 String newProviders = TextUtils.join(",", providers.toArray()); 145 return Settings.Secure.putString( 146 cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, newProviders); 147 } 148 149 @Implementation putInt(ContentResolver resolver, String name, int value)150 protected static boolean putInt(ContentResolver resolver, String name, int value) { 151 if (Settings.Secure.LOCATION_MODE.equals(name) 152 && RuntimeEnvironment.getApiLevel() >= LOLLIPOP 153 && RuntimeEnvironment.getApiLevel() <= P) { 154 // Map LOCATION_MODE to underlying location provider storage API 155 return Shadow.directlyOn( 156 Settings.Secure.class, 157 "setLocationModeForUser", 158 ClassParameter.from(ContentResolver.class, resolver), 159 ClassParameter.from(int.class, value), 160 ClassParameter.from(int.class, 0)); 161 } 162 return Shadow.directlyOn( 163 Settings.Secure.class, 164 "putInt", 165 ClassParameter.from(ContentResolver.class, resolver), 166 ClassParameter.from(String.class, name), 167 ClassParameter.from(int.class, value)); 168 } 169 170 @Implementation getInt(ContentResolver resolver, String name)171 protected static int getInt(ContentResolver resolver, String name) { 172 if (Settings.Secure.LOCATION_MODE.equals(name) 173 && RuntimeEnvironment.getApiLevel() >= LOLLIPOP 174 && RuntimeEnvironment.getApiLevel() <= P) { 175 // Map from to underlying location provider storage API to location mode 176 return Shadow.directlyOn( 177 Settings.Secure.class, 178 "getLocationModeForUser", 179 ClassParameter.from(ContentResolver.class, resolver), 180 ClassParameter.from(int.class, 0)); 181 } 182 183 return Shadow.directlyOn( 184 Settings.Secure.class, 185 "getInt", 186 ClassParameter.from(ContentResolver.class, resolver), 187 ClassParameter.from(String.class, name)); 188 } 189 190 @Implementation getInt(ContentResolver resolver, String name, int def)191 protected static int getInt(ContentResolver resolver, String name, int def) { 192 if (Settings.Secure.LOCATION_MODE.equals(name) 193 && RuntimeEnvironment.getApiLevel() >= LOLLIPOP 194 && RuntimeEnvironment.getApiLevel() <= P) { 195 // Map from to underlying location provider storage API to location mode 196 return Shadow.directlyOn( 197 Settings.Secure.class, 198 "getLocationModeForUser", 199 ClassParameter.from(ContentResolver.class, resolver), 200 ClassParameter.from(int.class, 0)); 201 } 202 203 return Shadow.directlyOn( 204 Settings.Secure.class, 205 "getInt", 206 ClassParameter.from(ContentResolver.class, resolver), 207 ClassParameter.from(String.class, name), 208 ClassParameter.from(int.class, def)); 209 } 210 } 211 212 @Implements(value = Settings.Global.class, minSdk = JELLY_BEAN_MR1) 213 public static class ShadowGlobal { 214 private static final Map<ContentResolver, Map<String, String>> dataMap = new WeakHashMap<>(); 215 216 @Resetter reset()217 public static void reset() { 218 dataMap.clear(); 219 } 220 221 @Implementation(minSdk = JELLY_BEAN_MR1) putStringForUser(ContentResolver cr, String name, String value, int userHandle)222 protected static boolean putStringForUser(ContentResolver cr, String name, String value, 223 int userHandle) { 224 return putString(cr, name, value); 225 } 226 227 @Implementation(minSdk = JELLY_BEAN_MR1) getStringForUser(ContentResolver cr, String name, int userHandle)228 protected static String getStringForUser(ContentResolver cr, String name, int userHandle) { 229 return getString(cr, name); 230 } 231 232 @Implementation putString(ContentResolver cr, String name, String value)233 protected static boolean putString(ContentResolver cr, String name, String value) { 234 get(cr).put(name, value); 235 return true; 236 } 237 238 @Implementation getString(ContentResolver cr, String name)239 protected static String getString(ContentResolver cr, String name) { 240 return get(cr).get(name); 241 } 242 get(ContentResolver cr)243 private static Map<String, String> get(ContentResolver cr) { 244 Map<String, String> map = dataMap.get(cr); 245 if (map == null) { 246 map = new HashMap<>(); 247 dataMap.put(cr, map); 248 } 249 return map; 250 } 251 } 252 253 @Implements(value = Settings.Config.class, minSdk = Build.VERSION_CODES.Q) 254 public static class ShadowConfig { 255 private static final Map<ContentResolver, Map<String, String>> dataMap = new WeakHashMap<>(); 256 257 @Resetter reset()258 public static void reset() { 259 dataMap.clear(); 260 } 261 262 @Implementation putString(ContentResolver cr, String name, String value)263 protected static boolean putString(ContentResolver cr, String name, String value) { 264 get(cr).put(name, value); 265 return true; 266 } 267 268 @Implementation getString(ContentResolver cr, String name)269 protected static String getString(ContentResolver cr, String name) { 270 return get(cr).get(name); 271 } 272 273 // BEGIN-INTERNAL 274 @Implementation(minSdk = Build.VERSION_CODES.R) getStrings(ContentResolver cr, String prefix, List<String> names)275 protected static Map<String, String> getStrings(ContentResolver cr, String prefix, 276 List<String> names) { 277 List<String> concatNames = new ArrayList<>(); 278 for (String name : names) { 279 concatNames.add(prefix + "/" + name); 280 } 281 282 Map<String, String> values = get(cr); 283 Map<String, String> arrayMap = new ArrayMap<>(); 284 for (String name : concatNames) { 285 if (values.containsKey(name)) { 286 arrayMap.put(name, values.get(name)); 287 } 288 } 289 return arrayMap; 290 } 291 // END-INTERNAL 292 get(ContentResolver cr)293 private static Map<String, String> get(ContentResolver cr) { 294 Map<String, String> map = dataMap.get(cr); 295 if (map == null) { 296 map = new HashMap<>(); 297 dataMap.put(cr, map); 298 } 299 return map; 300 } 301 } 302 303 /** 304 * Sets the value of the {@link Settings.System#AIRPLANE_MODE_ON} setting. 305 * 306 * @param isAirplaneMode new status for airplane mode 307 */ setAirplaneMode(boolean isAirplaneMode)308 public static void setAirplaneMode(boolean isAirplaneMode) { 309 Settings.Global.putInt( 310 RuntimeEnvironment.application.getContentResolver(), 311 Settings.Global.AIRPLANE_MODE_ON, 312 isAirplaneMode ? 1 : 0); 313 Settings.System.putInt( 314 RuntimeEnvironment.application.getContentResolver(), 315 Settings.System.AIRPLANE_MODE_ON, 316 isAirplaneMode ? 1 : 0); 317 } 318 319 /** 320 * Non-Android accessor that allows the value of the WIFI_ON setting to be set. 321 * 322 * @param isOn new status for wifi mode 323 */ setWifiOn(boolean isOn)324 public static void setWifiOn(boolean isOn) { 325 Settings.Global.putInt( 326 RuntimeEnvironment.application.getContentResolver(), Settings.Global.WIFI_ON, isOn ? 1 : 0); 327 Settings.System.putInt( 328 RuntimeEnvironment.application.getContentResolver(), Settings.System.WIFI_ON, isOn ? 1 : 0); 329 } 330 331 /** 332 * Sets the value of the {@link Settings.System#TIME_12_24} setting. 333 * 334 * @param use24HourTimeFormat new status for the time setting 335 */ set24HourTimeFormat(boolean use24HourTimeFormat)336 public static void set24HourTimeFormat(boolean use24HourTimeFormat) { 337 Settings.System.putString(RuntimeEnvironment.application.getContentResolver(), Settings.System.TIME_12_24, use24HourTimeFormat ? "24" : "12"); 338 } 339 340 private static boolean canDrawOverlays = false; 341 342 /** @return `false` by default, or the value specified via {@link #setCanDrawOverlays(boolean)} */ 343 @Implementation(minSdk = M) canDrawOverlays(Context context)344 protected static boolean canDrawOverlays(Context context) { 345 return canDrawOverlays; 346 } 347 348 /** Sets the value returned by {@link #canDrawOverlays(Context)}. */ setCanDrawOverlays(boolean canDrawOverlays)349 public static void setCanDrawOverlays(boolean canDrawOverlays) { 350 ShadowSettings.canDrawOverlays = canDrawOverlays; 351 } 352 353 /** 354 * Sets the value of the {@link Settings.Global#ADB_ENABLED} setting or {@link 355 * Settings.Secure#ADB_ENABLED} depending on API level. 356 * 357 * @param adbEnabled new value for whether adb is enabled 358 */ setAdbEnabled(boolean adbEnabled)359 public static void setAdbEnabled(boolean adbEnabled) { 360 // This setting moved from Secure to Global in JELLY_BEAN_MR1 361 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 362 Settings.Global.putInt( 363 RuntimeEnvironment.application.getContentResolver(), 364 Settings.Global.ADB_ENABLED, 365 adbEnabled ? 1 : 0); 366 } 367 // Support all clients by always setting the Secure version of the setting 368 Settings.Secure.putInt( 369 RuntimeEnvironment.application.getContentResolver(), 370 Settings.Secure.ADB_ENABLED, 371 adbEnabled ? 1 : 0); 372 } 373 374 /** 375 * Sets the value of the {@link Settings.Global#INSTALL_NON_MARKET_APPS} setting or {@link 376 * Settings.Secure#INSTALL_NON_MARKET_APPS} depending on API level. 377 * 378 * @param installNonMarketApps new value for whether non-market apps are allowed to be installed 379 */ setInstallNonMarketApps(boolean installNonMarketApps)380 public static void setInstallNonMarketApps(boolean installNonMarketApps) { 381 // This setting moved from Secure to Global in JELLY_BEAN_MR1 and then moved it back to Global 382 // in LOLLIPOP. Support all clients by always setting this field on all versions >= 383 // JELLY_BEAN_MR1. 384 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 385 Settings.Global.putInt( 386 RuntimeEnvironment.application.getContentResolver(), 387 Settings.Global.INSTALL_NON_MARKET_APPS, 388 installNonMarketApps ? 1 : 0); 389 } 390 // Always set the Secure version of the setting 391 Settings.Secure.putInt( 392 RuntimeEnvironment.application.getContentResolver(), 393 Settings.Secure.INSTALL_NON_MARKET_APPS, 394 installNonMarketApps ? 1 : 0); 395 } 396 397 @Resetter reset()398 public static void reset() { 399 canDrawOverlays = false; 400 } 401 } 402