1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 import static android.os.Build.VERSION_CODES.M; 6 import static android.os.Build.VERSION_CODES.N; 7 import static android.os.Build.VERSION_CODES.N_MR1; 8 import static org.robolectric.shadow.api.Shadow.directlyOn; 9 import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf; 10 11 import android.content.res.AssetFileDescriptor; 12 import android.content.res.AssetManager; 13 import android.content.res.Configuration; 14 import android.content.res.Resources; 15 import android.content.res.Resources.NotFoundException; 16 import android.content.res.ResourcesImpl; 17 import android.content.res.TypedArray; 18 import android.content.res.XmlResourceParser; 19 import android.graphics.Bitmap; 20 import android.graphics.drawable.BitmapDrawable; 21 import android.graphics.drawable.Drawable; 22 import android.os.ParcelFileDescriptor; 23 import android.util.AttributeSet; 24 import android.util.DisplayMetrics; 25 import android.util.LongSparseArray; 26 import android.util.TypedValue; 27 import java.io.FileInputStream; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.lang.reflect.Field; 31 import java.lang.reflect.Modifier; 32 import java.util.ArrayList; 33 import java.util.List; 34 import java.util.Locale; 35 import org.robolectric.RuntimeEnvironment; 36 import org.robolectric.annotation.HiddenApi; 37 import org.robolectric.annotation.Implementation; 38 import org.robolectric.annotation.Implements; 39 import org.robolectric.annotation.RealObject; 40 import org.robolectric.annotation.Resetter; 41 import org.robolectric.res.Plural; 42 import org.robolectric.res.PluralRules; 43 import org.robolectric.res.ResName; 44 import org.robolectric.res.ResType; 45 import org.robolectric.res.ResourceTable; 46 import org.robolectric.res.TypedResource; 47 import org.robolectric.shadow.api.Shadow; 48 import org.robolectric.shadows.ShadowLegacyResourcesImpl.ShadowLegacyThemeImpl; 49 import org.robolectric.util.ReflectionHelpers; 50 import org.robolectric.util.ReflectionHelpers.ClassParameter; 51 52 @Implements(Resources.class) 53 public class ShadowResources { 54 55 private static Resources system = null; 56 private static List<LongSparseArray<?>> resettableArrays; 57 58 @RealObject Resources realResources; 59 60 @Resetter reset()61 public static void reset() { 62 if (resettableArrays == null) { 63 resettableArrays = obtainResettableArrays(); 64 } 65 for (LongSparseArray<?> sparseArray : resettableArrays) { 66 sparseArray.clear(); 67 } 68 system = null; 69 70 ReflectionHelpers.setStaticField(Resources.class, "mSystem", null); 71 } 72 73 @Implementation getSystem()74 protected static Resources getSystem() { 75 if (system == null) { 76 AssetManager assetManager = AssetManager.getSystem(); 77 DisplayMetrics metrics = new DisplayMetrics(); 78 Configuration config = new Configuration(); 79 system = new Resources(assetManager, metrics, config); 80 } 81 return system; 82 } 83 84 @Implementation obtainAttributes(AttributeSet set, int[] attrs)85 protected TypedArray obtainAttributes(AttributeSet set, int[] attrs) { 86 if (isLegacyAssetManager()) { 87 return legacyShadowOf(realResources.getAssets()) 88 .attrsToTypedArray(realResources, set, attrs, 0, 0, 0); 89 } else { 90 return directlyOn(realResources, Resources.class).obtainAttributes(set, attrs); 91 } 92 } 93 94 @Implementation getQuantityString(int id, int quantity, Object... formatArgs)95 protected String getQuantityString(int id, int quantity, Object... formatArgs) 96 throws Resources.NotFoundException { 97 if (isLegacyAssetManager()) { 98 String raw = getQuantityString(id, quantity); 99 return String.format(Locale.ENGLISH, raw, formatArgs); 100 } else { 101 return directlyOn(realResources, Resources.class).getQuantityString(id, quantity, formatArgs); 102 } 103 } 104 105 @Implementation getQuantityString(int resId, int quantity)106 protected String getQuantityString(int resId, int quantity) throws Resources.NotFoundException { 107 if (isLegacyAssetManager()) { 108 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets()); 109 110 TypedResource typedResource = shadowAssetManager.getResourceTable() 111 .getValue(resId, shadowAssetManager.config); 112 if (typedResource != null && typedResource instanceof PluralRules) { 113 PluralRules pluralRules = (PluralRules) typedResource; 114 Plural plural = pluralRules.find(quantity); 115 116 if (plural == null) { 117 return null; 118 } 119 120 TypedResource<?> resolvedTypedResource = shadowAssetManager.resolve( 121 new TypedResource<>(plural.getString(), ResType.CHAR_SEQUENCE, pluralRules.getXmlContext()), 122 shadowAssetManager.config, resId); 123 return resolvedTypedResource == null ? null : resolvedTypedResource.asString(); 124 } else { 125 return null; 126 } 127 } else { 128 return directlyOn(realResources, Resources.class).getQuantityString(resId, quantity); 129 } 130 } 131 132 @Implementation openRawResource(int id)133 protected InputStream openRawResource(int id) throws Resources.NotFoundException { 134 if (isLegacyAssetManager()) { 135 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets()); 136 ResourceTable resourceTable = shadowAssetManager.getResourceTable(); 137 InputStream inputStream = resourceTable.getRawValue(id, shadowAssetManager.config); 138 if (inputStream == null) { 139 throw newNotFoundException(id); 140 } else { 141 return inputStream; 142 } 143 } else { 144 return directlyOn(realResources, Resources.class).openRawResource(id); 145 } 146 } 147 148 /** 149 * Since {@link AssetFileDescriptor}s are not yet supported by Robolectric, {@code null} will be 150 * returned if the resource is found. If the resource cannot be found, {@link 151 * Resources.NotFoundException} will be thrown. 152 */ 153 @Implementation openRawResourceFd(int id)154 protected AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException { 155 if (isLegacyAssetManager()) { 156 InputStream inputStream = openRawResource(id); 157 if (!(inputStream instanceof FileInputStream)) { 158 // todo fixme 159 return null; 160 } 161 162 FileInputStream fis = (FileInputStream) inputStream; 163 try { 164 return new AssetFileDescriptor(ParcelFileDescriptor.dup(fis.getFD()), 0, 165 fis.getChannel().size()); 166 } catch (IOException e) { 167 throw newNotFoundException(id); 168 } 169 } else { 170 return directlyOn(realResources, Resources.class).openRawResourceFd(id); 171 } 172 } 173 newNotFoundException(int id)174 private Resources.NotFoundException newNotFoundException(int id) { 175 ResourceTable resourceTable = legacyShadowOf(realResources.getAssets()).getResourceTable(); 176 ResName resName = resourceTable.getResName(id); 177 if (resName == null) { 178 return new Resources.NotFoundException("resource ID #0x" + Integer.toHexString(id)); 179 } else { 180 return new Resources.NotFoundException(resName.getFullyQualifiedName()); 181 } 182 } 183 184 @Implementation obtainTypedArray(int id)185 protected TypedArray obtainTypedArray(int id) throws Resources.NotFoundException { 186 if (isLegacyAssetManager()) { 187 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets()); 188 TypedArray typedArray = shadowAssetManager.getTypedArrayResource(realResources, id); 189 if (typedArray != null) { 190 return typedArray; 191 } else { 192 throw newNotFoundException(id); 193 } 194 } else { 195 return directlyOn(realResources, Resources.class).obtainTypedArray(id); 196 } 197 } 198 199 @HiddenApi 200 @Implementation loadXmlResourceParser(int resId, String type)201 protected XmlResourceParser loadXmlResourceParser(int resId, String type) 202 throws Resources.NotFoundException { 203 if (isLegacyAssetManager()) { 204 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets()); 205 return shadowAssetManager.loadXmlResourceParser(resId, type); 206 } else { 207 return directlyOn(realResources, Resources.class, "loadXmlResourceParser", 208 ClassParameter.from(int.class, resId), 209 ClassParameter.from(String.class, type)); 210 } 211 } 212 213 @HiddenApi 214 @Implementation loadXmlResourceParser( String file, int id, int assetCookie, String type)215 protected XmlResourceParser loadXmlResourceParser( 216 String file, int id, int assetCookie, String type) throws Resources.NotFoundException { 217 if (isLegacyAssetManager()) { 218 return loadXmlResourceParser(id, type); 219 } else { 220 return directlyOn(realResources, Resources.class, "loadXmlResourceParser", 221 ClassParameter.from(String.class, file), 222 ClassParameter.from(int.class, id), 223 ClassParameter.from(int.class, assetCookie), 224 ClassParameter.from(String.class, type)); 225 } 226 } 227 228 @HiddenApi 229 @Implementation(maxSdk = KITKAT_WATCH) loadDrawable(TypedValue value, int id)230 protected Drawable loadDrawable(TypedValue value, int id) { 231 Drawable drawable = directlyOn(realResources, Resources.class, "loadDrawable", 232 ClassParameter.from(TypedValue.class, value), 233 ClassParameter.from(int.class, id)); 234 setCreatedFromResId(realResources, id, drawable); 235 return drawable; 236 } 237 238 @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1) loadDrawable(TypedValue value, int id, Resources.Theme theme)239 protected Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme) 240 throws Resources.NotFoundException { 241 Drawable drawable = directlyOn(realResources, Resources.class, "loadDrawable", 242 ClassParameter.from(TypedValue.class, value), ClassParameter.from(int.class, id), ClassParameter.from(Resources.Theme.class, theme)); 243 setCreatedFromResId(realResources, id, drawable); 244 return drawable; 245 } 246 obtainResettableArrays()247 private static List<LongSparseArray<?>> obtainResettableArrays() { 248 List<LongSparseArray<?>> resettableArrays = new ArrayList<>(); 249 Field[] allFields = Resources.class.getDeclaredFields(); 250 for (Field field : allFields) { 251 if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(LongSparseArray.class)) { 252 field.setAccessible(true); 253 try { 254 LongSparseArray<?> longSparseArray = (LongSparseArray<?>) field.get(null); 255 if (longSparseArray != null) { 256 resettableArrays.add(longSparseArray); 257 } 258 } catch (IllegalAccessException e) { 259 throw new RuntimeException(e); 260 } 261 } 262 } 263 return resettableArrays; 264 } 265 266 public static abstract class ShadowTheme { 267 268 public static class Picker extends ResourceModeShadowPicker<ShadowTheme> { 269 Picker()270 public Picker() { 271 super(ShadowLegacyTheme.class, null, null); 272 } 273 } 274 } 275 276 @Implements(value = Resources.Theme.class, shadowPicker = ShadowTheme.Picker.class) 277 public static class ShadowLegacyTheme extends ShadowTheme { 278 @RealObject Resources.Theme realTheme; 279 getNativePtr()280 long getNativePtr() { 281 if (RuntimeEnvironment.getApiLevel() >= N) { 282 ResourcesImpl.ThemeImpl themeImpl = ReflectionHelpers.getField(realTheme, "mThemeImpl"); 283 return ((ShadowLegacyThemeImpl) Shadow.extract(themeImpl)).getNativePtr(); 284 } else { 285 return ((Number) ReflectionHelpers.getField(realTheme, "mTheme")).longValue(); 286 } 287 } 288 289 @Implementation(maxSdk = M) obtainStyledAttributes(int[] attrs)290 protected TypedArray obtainStyledAttributes(int[] attrs) { 291 return obtainStyledAttributes(0, attrs); 292 } 293 294 @Implementation(maxSdk = M) obtainStyledAttributes(int resid, int[] attrs)295 protected TypedArray obtainStyledAttributes(int resid, int[] attrs) 296 throws Resources.NotFoundException { 297 return obtainStyledAttributes(null, attrs, 0, resid); 298 } 299 300 @Implementation(maxSdk = M) obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)301 protected TypedArray obtainStyledAttributes( 302 AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { 303 return getShadowAssetManager().attrsToTypedArray(getResources(), set, attrs, defStyleAttr, getNativePtr(), defStyleRes); 304 } 305 getShadowAssetManager()306 private ShadowLegacyAssetManager getShadowAssetManager() { 307 return legacyShadowOf(getResources().getAssets()); 308 } 309 getResources()310 private Resources getResources() { 311 return ReflectionHelpers.getField(realTheme, "this$0"); 312 } 313 } 314 315 setCreatedFromResId(Resources resources, int id, Drawable drawable)316 static void setCreatedFromResId(Resources resources, int id, Drawable drawable) { 317 // todo: this kinda sucks, find some better way... 318 if (drawable != null && Shadow.extract(drawable) instanceof ShadowDrawable) { 319 ShadowDrawable shadowDrawable = Shadow.extract(drawable); 320 shadowDrawable.createdFromResId = id; 321 if (drawable instanceof BitmapDrawable) { 322 Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); 323 if (bitmap != null && Shadow.extract(bitmap) instanceof ShadowBitmap) { 324 ShadowBitmap shadowBitmap = Shadow.extract(bitmap); 325 if (shadowBitmap.createdFromResId == -1) { 326 String resourceName; 327 try { 328 resourceName = resources.getResourceName(id); 329 } catch (NotFoundException e) { 330 resourceName = "Unknown resource #0x" + Integer.toHexString(id); 331 } 332 shadowBitmap.setCreatedFromResId(id, resourceName); 333 } 334 } 335 } 336 } 337 } 338 isLegacyAssetManager()339 private boolean isLegacyAssetManager() { 340 return ShadowAssetManager.useLegacy(); 341 } 342 343 @Implements(Resources.NotFoundException.class) 344 public static class ShadowNotFoundException { 345 @RealObject Resources.NotFoundException realObject; 346 347 private String message; 348 349 @Implementation __constructor__()350 protected void __constructor__() {} 351 352 @Implementation __constructor__(String name)353 protected void __constructor__(String name) { 354 this.message = name; 355 } 356 357 @Override @Implementation toString()358 public String toString() { 359 return realObject.getClass().getName() + ": " + message; 360 } 361 } 362 } 363