• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.settings.testutils.shadow;
2 
3 import static android.util.TypedValue.TYPE_REFERENCE;
4 import static org.robolectric.RuntimeEnvironment.application;
5 import static org.robolectric.Shadows.shadowOf;
6 import static org.robolectric.shadow.api.Shadow.directlyOn;
7 
8 import android.annotation.DimenRes;
9 import android.content.res.ColorStateList;
10 import android.content.res.Resources;
11 import android.content.res.Resources.NotFoundException;
12 import android.content.res.Resources.Theme;
13 import android.content.res.TypedArray;
14 import android.graphics.Color;
15 import android.graphics.drawable.ColorDrawable;
16 import android.graphics.drawable.Drawable;
17 import android.support.annotation.ArrayRes;
18 import android.support.annotation.ColorRes;
19 import android.support.annotation.Nullable;
20 import android.util.AttributeSet;
21 import android.util.SparseArray;
22 import android.util.TypedValue;
23 
24 import com.android.settings.R;
25 
26 import org.robolectric.RuntimeEnvironment;
27 import org.robolectric.android.XmlResourceParserImpl;
28 import org.robolectric.annotation.Implementation;
29 import org.robolectric.annotation.Implements;
30 import org.robolectric.annotation.RealObject;
31 import org.robolectric.res.StyleData;
32 import org.robolectric.res.StyleResolver;
33 import org.robolectric.res.ThemeStyleSet;
34 import org.robolectric.shadows.ShadowAssetManager;
35 import org.robolectric.shadows.ShadowResources;
36 import org.robolectric.util.ReflectionHelpers;
37 import org.robolectric.util.ReflectionHelpers.ClassParameter;
38 import org.w3c.dom.Node;
39 
40 import java.util.List;
41 import java.util.Map;
42 
43 /**
44  * Shadow Resources and Theme classes to handle resource references that Robolectric shadows cannot
45  * handle because they are too new or private.
46  */
47 @Implements(value = Resources.class, inheritImplementationMethods = true)
48 public class SettingsShadowResources extends ShadowResources {
49 
50     @RealObject
51     public Resources realResources;
52 
53     private static SparseArray<Object> sResourceOverrides = new SparseArray<>();
54 
overrideResource(int id, Object value)55     public static void overrideResource(int id, Object value) {
56         synchronized (sResourceOverrides) {
57             sResourceOverrides.put(id, value);
58         }
59     }
60 
overrideResource(String name, Object value)61     public static void overrideResource(String name, Object value) {
62         final Resources res = application.getResources();
63         final int resId = res.getIdentifier(name, null, null);
64         if (resId == 0) {
65             throw new Resources.NotFoundException("Cannot override \"" + name + "\"");
66         }
67         overrideResource(resId, value);
68     }
69 
reset()70     public static void reset() {
71         synchronized (sResourceOverrides) {
72             sResourceOverrides.clear();
73         }
74     }
75 
76     @Implementation
getDimensionPixelSize(@imenRes int id)77     public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
78         // Handle requests for private dimension resources,
79         // TODO: Consider making a set of private dimension resource ids if this happens repeatedly.
80         if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) {
81             return 0;
82         }
83         return directlyOn(realResources, Resources.class).getDimensionPixelSize(id);
84     }
85 
86     @Implementation
getColor(@olorRes int id, @Nullable Theme theme)87     public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
88         if (id == R.color.battery_icon_color_error) {
89             return Color.WHITE;
90         }
91         return directlyOn(realResources, Resources.class).getColor(id, theme);
92     }
93 
94     @Implementation
getColorStateList(@olorRes int id, @Nullable Theme theme)95     public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme)
96             throws NotFoundException {
97         if (id == com.android.internal.R.color.text_color_primary) {
98             return ColorStateList.valueOf(Color.WHITE);
99         }
100         return directlyOn(realResources, Resources.class).getColorStateList(id, theme);
101     }
102 
103     /**
104      * Deprecated because SDK 24+ uses
105      * {@link SettingsShadowResourcesImpl#loadDrawable(Resources, TypedValue, int, int, Theme)}
106      *
107      * TODO: Delete when all tests have been migrated to sdk 26
108      */
109     @Deprecated
110     @Implementation
loadDrawable(TypedValue value, int id, Theme theme)111     public Drawable loadDrawable(TypedValue value, int id, Theme theme)
112             throws NotFoundException {
113         // The drawable item in switchbar_background.xml refers to a very recent color attribute
114         // that Robolectric isn't yet aware of.
115         // TODO: Remove this once Robolectric is updated.
116         if (id == R.drawable.switchbar_background) {
117             return new ColorDrawable();
118         } else if (id == R.drawable.ic_launcher_settings) {
119             // ic_launcher_settings uses adaptive-icon, which is not supported by robolectric,
120             // change it to a normal drawable.
121             id = R.drawable.ic_settings_wireless;
122         } else if (id == R.drawable.app_filter_spinner_background) {
123             id = R.drawable.ic_expand_more_inverse;
124         }
125         return super.loadDrawable(value, id, theme);
126     }
127 
128     @Implementation
getIntArray(@rrayRes int id)129     public int[] getIntArray(@ArrayRes int id) throws NotFoundException {
130         // The Robolectric isn't aware of resources in settingslib, so we need to stub it here
131         if (id == com.android.settings.R.array.batterymeter_bolt_points
132                 || id == com.android.settings.R.array.batterymeter_plus_points) {
133             return new int[2];
134         }
135 
136         final Object override;
137         synchronized (sResourceOverrides) {
138             override = sResourceOverrides.get(id);
139         }
140         if (override instanceof int[]) {
141             return (int[]) override;
142         }
143         return directlyOn(realResources, Resources.class).getIntArray(id);
144     }
145 
146     @Implementation
getString(int id)147     public String getString(int id) {
148         final Object override;
149         synchronized (sResourceOverrides) {
150             override = sResourceOverrides.get(id);
151         }
152         if (override instanceof String) {
153             return (String) override;
154         }
155         return directlyOn(
156                 realResources, Resources.class, "getString", ClassParameter.from(int.class, id));
157     }
158 
159     @Implementation
getInteger(int id)160     public int getInteger(int id) {
161         final Object override;
162         synchronized (sResourceOverrides) {
163             override = sResourceOverrides.get(id);
164         }
165         if (override instanceof Integer) {
166             return (Integer) override;
167         }
168         return directlyOn(
169                 realResources, Resources.class, "getInteger", ClassParameter.from(int.class, id));
170     }
171 
172     @Implementation
getBoolean(int id)173     public boolean getBoolean(int id) {
174         final Object override;
175         synchronized (sResourceOverrides) {
176             override = sResourceOverrides.get(id);
177         }
178         if (override instanceof Boolean) {
179             return (boolean) override;
180         }
181         return directlyOn(realResources, Resources.class, "getBoolean",
182                 ClassParameter.from(int.class, id));
183     }
184 
185     @Implements(value = Theme.class, inheritImplementationMethods = true)
186     public static class SettingsShadowTheme extends ShadowTheme {
187 
188         @RealObject
189         Theme realTheme;
190 
191         private ShadowAssetManager mAssetManager = shadowOf(
192                 RuntimeEnvironment.application.getAssets());
193 
194         @Implementation
obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)195         public TypedArray obtainStyledAttributes(
196                 AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
197             // Replace all private string references with a placeholder.
198             if (set != null) {
199                 synchronized (set) {
200                     for (int i = 0; i < set.getAttributeCount(); ++i) {
201                         final String attributeValue = set.getAttributeValue(i);
202                         final Node node = ReflectionHelpers.callInstanceMethod(
203                                 XmlResourceParserImpl.class, set, "getAttributeAt",
204                                 ReflectionHelpers.ClassParameter.from(int.class, i));
205                         if (attributeValue.contains("attr/fingerprint_layout_theme")) {
206                             // Workaround for https://github.com/robolectric/robolectric/issues/2641
207                             node.setNodeValue("@style/FingerprintLayoutTheme");
208                         } else if (attributeValue.startsWith("@*android:string")) {
209                             node.setNodeValue("PLACEHOLDER");
210                         }
211                     }
212                 }
213             }
214 
215             // Track down all styles and remove all inheritance from private styles.
216             final Map<Long, Object /* NativeTheme */> appliedStylesList =
217                     ReflectionHelpers.getField(mAssetManager, "nativeThemes");
218             synchronized (appliedStylesList) {
219                 for (Long idx : appliedStylesList.keySet()) {
220                     final ThemeStyleSet appliedStyles = ReflectionHelpers.getField(
221                             appliedStylesList.get(idx), "themeStyleSet");
222                     // The Object's below are actually ShadowAssetManager.OverlayedStyle.
223                     // We can't use
224 
225                     // it here because it's private.
226                     final List<Object /* OverlayedStyle */> overlayedStyles =
227                             ReflectionHelpers.getField(appliedStyles, "styles");
228                     for (Object appliedStyle : overlayedStyles) {
229                         final StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle,
230                                 "style");
231                         final List<StyleData> styleDatas =
232                                 ReflectionHelpers.getField(styleResolver, "styles");
233                         for (StyleData styleData : styleDatas) {
234                             if (styleData.getParent() != null &&
235                                     styleData.getParent().startsWith("@*android:style")) {
236                                 ReflectionHelpers.setField(StyleData.class, styleData, "parent",
237                                         null);
238                             }
239                         }
240                     }
241 
242                 }
243             }
244             return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
245         }
246 
247         @Implementation
resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs)248         public synchronized boolean resolveAttribute(int resid, TypedValue outValue,
249                 boolean resolveRefs) {
250             // The real Resources instance in Robolectric tests somehow fails to find the
251             // preferenceTheme attribute in the layout. Let's do it ourselves.
252             if (getResources().getResourceName(resid)
253                     .equals("com.android.settings:attr/preferenceTheme")) {
254                 final int preferenceThemeResId =
255                         getResources().getIdentifier(
256                                 "PreferenceTheme", "style", "com.android.settings");
257                 outValue.type = TYPE_REFERENCE;
258                 outValue.data = preferenceThemeResId;
259                 outValue.resourceId = preferenceThemeResId;
260                 return true;
261             }
262             return directlyOn(realTheme, Theme.class)
263                     .resolveAttribute(resid, outValue, resolveRefs);
264         }
265 
getResources()266         private Resources getResources() {
267             return ReflectionHelpers.callInstanceMethod(ShadowTheme.class, this, "getResources");
268         }
269     }
270 }
271