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