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