1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.N_MR1; 4 import static android.os.Build.VERSION_CODES.Q; 5 import static org.robolectric.util.reflector.Reflector.reflector; 6 7 import android.content.res.AssetFileDescriptor; 8 import android.content.res.AssetManager; 9 import android.content.res.CompatibilityInfo; 10 import android.content.res.Configuration; 11 import android.content.res.Resources; 12 import android.content.res.Resources.NotFoundException; 13 import android.content.res.TypedArray; 14 import android.content.res.XmlResourceParser; 15 import android.graphics.drawable.Drawable; 16 import android.util.AttributeSet; 17 import android.util.DisplayMetrics; 18 import android.util.LongSparseArray; 19 import android.util.TypedValue; 20 import java.io.InputStream; 21 import java.lang.reflect.Field; 22 import java.lang.reflect.Modifier; 23 import java.util.ArrayList; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Set; 27 import org.robolectric.RuntimeEnvironment; 28 import org.robolectric.android.Bootstrap; 29 import org.robolectric.annotation.HiddenApi; 30 import org.robolectric.annotation.Implementation; 31 import org.robolectric.annotation.Implements; 32 import org.robolectric.annotation.RealObject; 33 import org.robolectric.annotation.Resetter; 34 import org.robolectric.internal.bytecode.ShadowedObject; 35 import org.robolectric.shadow.api.Shadow; 36 import org.robolectric.util.ReflectionHelpers; 37 import org.robolectric.util.reflector.Direct; 38 import org.robolectric.util.reflector.ForType; 39 40 /** Shadow of {@link Resources}. */ 41 @Implements(Resources.class) 42 public class ShadowResources { 43 44 private static Resources system = null; 45 private static List<LongSparseArray<?>> resettableArrays; 46 47 @RealObject Resources realResources; 48 private final Set<OnConfigurationChangeListener> configurationChangeListeners = new HashSet<>(); 49 50 @Resetter reset()51 public static void reset() { 52 if (resettableArrays == null) { 53 resettableArrays = obtainResettableArrays(); 54 } 55 for (LongSparseArray<?> sparseArray : resettableArrays) { 56 sparseArray.clear(); 57 } 58 system = null; 59 60 ReflectionHelpers.setStaticField(Resources.class, "mSystem", null); 61 } 62 63 @Implementation getSystem()64 protected static Resources getSystem() { 65 if (system == null) { 66 AssetManager assetManager = AssetManager.getSystem(); 67 system = 68 new Resources(assetManager, Bootstrap.getDisplayMetrics(), Bootstrap.getConfiguration()); 69 } 70 return system; 71 } 72 73 @HiddenApi 74 @Implementation loadXmlResourceParser(int resId, String type)75 protected XmlResourceParser loadXmlResourceParser(int resId, String type) 76 throws Resources.NotFoundException { 77 78 ResourcesReflector relectedResources = reflector(ResourcesReflector.class, realResources); 79 return setSourceResourceId(relectedResources.loadXmlResourceParser(resId, type), resId); 80 } 81 82 @HiddenApi 83 @Implementation loadXmlResourceParser( String file, int id, int assetCookie, String type)84 protected XmlResourceParser loadXmlResourceParser( 85 String file, int id, int assetCookie, String type) throws Resources.NotFoundException { 86 87 ResourcesReflector relectedResources = reflector(ResourcesReflector.class, realResources); 88 return setSourceResourceId( 89 relectedResources.loadXmlResourceParser(file, id, assetCookie, type), id); 90 } 91 setSourceResourceId(XmlResourceParser parser, int resourceId)92 private static XmlResourceParser setSourceResourceId(XmlResourceParser parser, int resourceId) { 93 Object shadow = parser instanceof ShadowedObject ? Shadow.extract(parser) : null; 94 if (shadow instanceof ShadowXmlBlock.ShadowParser) { 95 ((ShadowXmlBlock.ShadowParser) shadow).setSourceResourceId(resourceId); 96 } 97 return parser; 98 } 99 100 @Implementation(maxSdk = N_MR1) loadDrawable(TypedValue value, int id, Resources.Theme theme)101 protected Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme) 102 throws Resources.NotFoundException { 103 Drawable drawable = 104 reflector(ResourcesReflector.class, realResources).loadDrawable(value, id, theme); 105 setCreatedFromResId(realResources, id, drawable); 106 return drawable; 107 } 108 obtainResettableArrays()109 private static List<LongSparseArray<?>> obtainResettableArrays() { 110 List<LongSparseArray<?>> resettableArrays = new ArrayList<>(); 111 Field[] allFields = Resources.class.getDeclaredFields(); 112 for (Field field : allFields) { 113 if (Modifier.isStatic(field.getModifiers()) 114 && field.getType().equals(LongSparseArray.class)) { 115 field.setAccessible(true); 116 try { 117 LongSparseArray<?> longSparseArray = (LongSparseArray<?>) field.get(null); 118 if (longSparseArray != null) { 119 resettableArrays.add(longSparseArray); 120 } 121 } catch (IllegalAccessException e) { 122 throw new RuntimeException(e); 123 } 124 } 125 } 126 return resettableArrays; 127 } 128 129 /** 130 * Returns the layout resource id the attribute set was inflated from. Backwards compatible 131 * version of {@link Resources#getAttributeSetSourceResId(AttributeSet)}, passes through to the 132 * underlying implementation on API levels where it is supported. 133 */ 134 @Implementation(minSdk = Q) getAttributeSetSourceResId(AttributeSet attrs)135 public static int getAttributeSetSourceResId(AttributeSet attrs) { 136 if (RuntimeEnvironment.getApiLevel() >= Q) { 137 return reflector(ResourcesReflector.class).getAttributeSetSourceResId(attrs); 138 } else { 139 Object shadow = attrs instanceof ShadowedObject ? Shadow.extract(attrs) : null; 140 return shadow instanceof ShadowXmlBlock.ShadowParser 141 ? ((ShadowXmlBlock.ShadowParser) shadow).getSourceResourceId() 142 : 0; 143 } 144 } 145 146 /** 147 * Listener callback that's called when the configuration is updated for a resources. The callback 148 * receives the old and new configs (and can use {@link Configuration#diff(Configuration)} to 149 * produce a diff). The callback is called after the configuration has been applied to the 150 * underlying resources, so obtaining resources will use the new configuration in the callback. 151 */ 152 public interface OnConfigurationChangeListener { onConfigurationChange( Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics)153 void onConfigurationChange( 154 Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics); 155 } 156 157 /** 158 * Add a listener to observe resource configuration changes. See {@link 159 * OnConfigurationChangeListener}. 160 */ addConfigurationChangeListener(OnConfigurationChangeListener listener)161 public void addConfigurationChangeListener(OnConfigurationChangeListener listener) { 162 configurationChangeListeners.add(listener); 163 } 164 165 /** 166 * Remove a listener to observe resource configuration changes. See {@link 167 * OnConfigurationChangeListener}. 168 */ removeConfigurationChangeListener(OnConfigurationChangeListener listener)169 public void removeConfigurationChangeListener(OnConfigurationChangeListener listener) { 170 configurationChangeListeners.remove(listener); 171 } 172 173 @Implementation updateConfiguration( Configuration config, DisplayMetrics metrics, CompatibilityInfo compat)174 protected void updateConfiguration( 175 Configuration config, DisplayMetrics metrics, CompatibilityInfo compat) { 176 Configuration oldConfig; 177 try { 178 oldConfig = new Configuration(realResources.getConfiguration()); 179 } catch (NullPointerException e) { 180 // In old versions of Android the resource constructor calls updateConfiguration, in the 181 // app compat ResourcesWrapper subclass the reference to the underlying resources hasn't been 182 // configured yet, so it'll throw an NPE, catch this to avoid crashing. 183 oldConfig = null; 184 } 185 reflector(ResourcesReflector.class, realResources).updateConfiguration(config, metrics, compat); 186 if (oldConfig != null && config != null) { 187 for (OnConfigurationChangeListener listener : configurationChangeListeners) { 188 listener.onConfigurationChange(oldConfig, config, metrics); 189 } 190 } 191 } 192 setCreatedFromResId(Resources resources, int id, Drawable drawable)193 static void setCreatedFromResId(Resources resources, int id, Drawable drawable) { 194 // todo: this kinda sucks, find some better way... 195 if (drawable != null && Shadow.extract(drawable) instanceof ShadowDrawable) { 196 ShadowDrawable shadowDrawable = Shadow.extract(drawable); 197 198 String resourceName; 199 try { 200 resourceName = resources.getResourceName(id); 201 } catch (NotFoundException e) { 202 resourceName = "Unknown resource #0x" + Integer.toHexString(id); 203 } 204 205 shadowDrawable.setCreatedFromResId(id, resourceName); 206 } 207 } 208 209 /** Shadow for {@link Resources.NotFoundException}. */ 210 @Implements(Resources.NotFoundException.class) 211 public static class ShadowNotFoundException { 212 @RealObject Resources.NotFoundException realObject; 213 214 private String message; 215 216 @Implementation __constructor__()217 protected void __constructor__() {} 218 219 @Implementation __constructor__(String name)220 protected void __constructor__(String name) { 221 this.message = name; 222 } 223 224 @Override 225 @Implementation toString()226 public String toString() { 227 return realObject.getClass().getName() + ": " + message; 228 } 229 } 230 231 @ForType(Resources.class) 232 interface ResourcesReflector { 233 234 @Direct loadXmlResourceParser(int resId, String type)235 XmlResourceParser loadXmlResourceParser(int resId, String type); 236 237 @Direct loadXmlResourceParser(String file, int id, int assetCookie, String type)238 XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type); 239 240 @Direct loadDrawable(TypedValue value, int id)241 Drawable loadDrawable(TypedValue value, int id); 242 243 @Direct loadDrawable(TypedValue value, int id, Resources.Theme theme)244 Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme); 245 246 @Direct obtainAttributes(AttributeSet set, int[] attrs)247 TypedArray obtainAttributes(AttributeSet set, int[] attrs); 248 249 @Direct getQuantityString(int id, int quantity, Object... formatArgs)250 String getQuantityString(int id, int quantity, Object... formatArgs); 251 252 @Direct getQuantityString(int resId, int quantity)253 String getQuantityString(int resId, int quantity); 254 255 @Direct openRawResource(int id)256 InputStream openRawResource(int id); 257 258 @Direct openRawResourceFd(int id)259 AssetFileDescriptor openRawResourceFd(int id); 260 261 @Direct obtainTypedArray(int id)262 TypedArray obtainTypedArray(int id); 263 264 @Direct getAttributeSetSourceResId(AttributeSet attrs)265 int getAttributeSetSourceResId(AttributeSet attrs); 266 267 @Direct updateConfiguration( Configuration config, DisplayMetrics metrics, CompatibilityInfo compat)268 void updateConfiguration( 269 Configuration config, DisplayMetrics metrics, CompatibilityInfo compat); 270 } 271 } 272