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