• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.settings.testutils.shadow;
2 
3 import static android.util.TypedValue.TYPE_REFERENCE;
4 
5 import static org.robolectric.RuntimeEnvironment.application;
6 import static org.robolectric.Shadows.shadowOf;
7 import static org.robolectric.internal.Shadow.directlyOn;
8 
9 import android.annotation.DimenRes;
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.annotation.Implementation;
28 import org.robolectric.annotation.Implements;
29 import org.robolectric.annotation.RealObject;
30 import org.robolectric.internal.Shadow;
31 import org.robolectric.res.StyleData;
32 import org.robolectric.res.StyleResolver;
33 import org.robolectric.res.builder.XmlResourceParserImpl;
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(Resources.class)
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         sResourceOverrides.put(id, value);
57     }
58 
overrideResource(String name, Object value)59     public static void overrideResource(String name, Object value) {
60         final Resources res = application.getResources();
61         final int resId = res.getIdentifier(name, null, null);
62         if (resId == 0) {
63             throw new Resources.NotFoundException("Cannot override \"" + name + "\"");
64         }
65         overrideResource(resId, value);
66     }
67 
reset()68     public static void reset() {
69         sResourceOverrides.clear();
70     }
71 
72     @Implementation
getDimensionPixelSize(@imenRes int id)73     public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
74         // Handle requests for private dimension resources,
75         // TODO: Consider making a set of private dimension resource ids if this happens repeatedly.
76         if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) {
77             return 0;
78         }
79         return directlyOn(realResources, Resources.class).getDimensionPixelSize(id);
80     }
81 
82     @Implementation
getColor(@olorRes int id, @Nullable Theme theme)83     public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
84         if (id == R.color.battery_icon_color_error) {
85             return Color.WHITE;
86         }
87         return directlyOn(realResources, Resources.class).getColor(id, theme);
88     }
89 
90     @Implementation
loadDrawable(TypedValue value, int id, Theme theme)91     public Drawable loadDrawable(TypedValue value, int id, Theme theme)
92             throws NotFoundException {
93         // The drawable item in switchbar_background.xml refers to a very recent color attribute
94         // that Robolectric isn't yet aware of.
95         // TODO: Remove this once Robolectric is updated.
96         if (id == R.drawable.switchbar_background) {
97             return new ColorDrawable();
98         } else if (id == R.drawable.ic_launcher_settings) {
99             // ic_launcher_settings uses adaptive-icon, which is not supported by robolectric,
100             // change it to a normal drawable.
101             id = R.drawable.ic_settings_wireless;
102         } else if (id == R.drawable.app_filter_spinner_background) {
103             id = R.drawable.ic_expand_more_inverse;
104         } else if (id == R.drawable.selectable_card_grey) {
105             id = R.drawable.ic_expand_more_inverse;
106         }
107         return super.loadDrawable(value, id, theme);
108     }
109 
110     @Implementation
getIntArray(@rrayRes int id)111     public int[] getIntArray(@ArrayRes int id) throws NotFoundException {
112         // The Robolectric isn't aware of resources in settingslib, so we need to stub it here
113         if (id == com.android.settings.R.array.batterymeter_bolt_points
114                 || id == com.android.settings.R.array.batterymeter_plus_points) {
115             return new int[2];
116         }
117         return directlyOn(realResources, Resources.class).getIntArray(id);
118     }
119 
120     @Implementation
getString(int id)121     public String getString(int id) {
122         final Object override = sResourceOverrides.get(id);
123         if (override instanceof String) {
124             return (String) override;
125         }
126         return Shadow.directlyOn(
127                 realResources, Resources.class, "getString", ClassParameter.from(int.class, id));
128     }
129 
130     @Implementation
getInteger(int id)131     public int getInteger(int id) {
132         final Object override = sResourceOverrides.get(id);
133         if (override instanceof Integer) {
134             return (Integer) override;
135         }
136         return Shadow.directlyOn(
137                 realResources, Resources.class, "getInteger", ClassParameter.from(int.class, id));
138     }
139 
140     @Implements(Theme.class)
141     public static class SettingsShadowTheme extends ShadowTheme {
142 
143         @RealObject
144         Theme realTheme;
145 
146         @Implementation
obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)147         public TypedArray obtainStyledAttributes(
148                 AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
149             // Replace all private string references with a placeholder.
150             if (set != null) {
151                 for (int i = 0; i < set.getAttributeCount(); ++i) {
152                     String attributeValue = set.getAttributeValue(i);
153                     Node node = ReflectionHelpers.callInstanceMethod(
154                             XmlResourceParserImpl.class, set, "getAttributeAt",
155                             ReflectionHelpers.ClassParameter.from(int.class, i));
156                     if (attributeValue.contains("attr/fingerprint_layout_theme")) {
157                         // Workaround for https://github.com/robolectric/robolectric/issues/2641
158                         node.setNodeValue("@style/FingerprintLayoutTheme");
159                     } else if (attributeValue.startsWith("@*android:string")) {
160                         node.setNodeValue("PLACEHOLDER");
161                     }
162                 }
163             }
164 
165             // Track down all styles and remove all inheritance from private styles.
166             ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets());
167             // The Object's below are actually ShadowAssetManager.OverlayedStyle. We can't use it
168             // here because it's package private.
169             Map<Long, List<Object>> appliedStylesList =
170                     ReflectionHelpers.getField(assetManager, "appliedStyles");
171             for (Long idx : appliedStylesList.keySet()) {
172                 List<Object> appliedStyles = appliedStylesList.get(idx);
173                 for (Object appliedStyle : appliedStyles) {
174                     StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle, "style");
175                     List<StyleData> styleDatas =
176                             ReflectionHelpers.getField(styleResolver, "styles");
177                     for (StyleData styleData : styleDatas) {
178                         if (styleData.getParent() != null &&
179                                 styleData.getParent().startsWith("@*android:style")) {
180                             ReflectionHelpers.setField(StyleData.class, styleData, "parent", null);
181                         }
182                     }
183                 }
184 
185             }
186             return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
187         }
188 
189         @Implementation
resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs)190         public boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) {
191             // The real Resources instance in Robolectric tests somehow fails to find the
192             // preferenceTheme attribute in the layout. Let's do it ourselves.
193             if (getResources().getResourceName(resid)
194                     .equals("com.android.settings:attr/preferenceTheme")) {
195                 int preferenceThemeResId =
196                         getResources().getIdentifier(
197                                 "PreferenceTheme", "style", "com.android.settings");
198                 outValue.type = TYPE_REFERENCE;
199                 outValue.data = preferenceThemeResId;
200                 outValue.resourceId = preferenceThemeResId;
201                 return true;
202             }
203             return directlyOn(realTheme, Theme.class)
204                     .resolveAttribute(resid, outValue, resolveRefs);
205         }
206     }
207 }
208