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