1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.M; 4 import static android.os.Build.VERSION_CODES.N; 5 import static android.os.Build.VERSION_CODES.N_MR1; 6 import static android.os.Build.VERSION_CODES.O; 7 import static org.robolectric.shadow.api.Shadow.directlyOn; 8 import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf; 9 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 10 11 import android.content.res.AssetFileDescriptor; 12 import android.content.res.AssetManager; 13 import android.content.res.Resources; 14 import android.content.res.ResourcesImpl; 15 import android.content.res.TypedArray; 16 import android.content.res.XmlResourceParser; 17 import android.graphics.drawable.Drawable; 18 import android.os.ParcelFileDescriptor; 19 import android.util.AttributeSet; 20 import android.util.TypedValue; 21 import java.io.FileInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.util.Locale; 25 import org.robolectric.RuntimeEnvironment; 26 import org.robolectric.annotation.HiddenApi; 27 import org.robolectric.annotation.Implementation; 28 import org.robolectric.annotation.Implements; 29 import org.robolectric.annotation.RealObject; 30 import org.robolectric.annotation.Resetter; 31 import org.robolectric.res.Plural; 32 import org.robolectric.res.PluralRules; 33 import org.robolectric.res.ResName; 34 import org.robolectric.res.ResType; 35 import org.robolectric.res.ResourceTable; 36 import org.robolectric.res.TypedResource; 37 import org.robolectric.shadows.ShadowResourcesImpl.Picker; 38 import org.robolectric.util.ReflectionHelpers; 39 40 @SuppressWarnings("NewApi") 41 @Implements(value = ResourcesImpl.class, isInAndroidSdk = false, minSdk = N, 42 shadowPicker = Picker.class) 43 public class ShadowLegacyResourcesImpl extends ShadowResourcesImpl { 44 45 @Resetter reset()46 public static void reset() { 47 if (RuntimeEnvironment.useLegacyResources()) { 48 ShadowResourcesImpl.reset(); 49 } 50 } 51 52 @RealObject 53 private ResourcesImpl realResourcesImpl; 54 55 56 @Implementation(maxSdk = M) getQuantityString(int id, int quantity, Object... formatArgs)57 public String getQuantityString(int id, int quantity, Object... formatArgs) throws Resources.NotFoundException { 58 String raw = getQuantityString(id, quantity); 59 return String.format(Locale.ENGLISH, raw, formatArgs); 60 } 61 62 @Implementation(maxSdk = M) getQuantityString(int resId, int quantity)63 public String getQuantityString(int resId, int quantity) throws Resources.NotFoundException { 64 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResourcesImpl.getAssets()); 65 66 TypedResource typedResource = shadowAssetManager.getResourceTable().getValue(resId, shadowAssetManager.config); 67 if (typedResource != null && typedResource instanceof PluralRules) { 68 PluralRules pluralRules = (PluralRules) typedResource; 69 Plural plural = pluralRules.find(quantity); 70 71 if (plural == null) { 72 return null; 73 } 74 75 TypedResource<?> resolvedTypedResource = shadowAssetManager.resolve( 76 new TypedResource<>(plural.getString(), ResType.CHAR_SEQUENCE, pluralRules.getXmlContext()), shadowAssetManager.config, resId); 77 return resolvedTypedResource == null ? null : resolvedTypedResource.asString(); 78 } else { 79 return null; 80 } 81 } 82 83 @Implementation(maxSdk = M) openRawResource(int id)84 public InputStream openRawResource(int id) throws Resources.NotFoundException { 85 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResourcesImpl.getAssets()); 86 ResourceTable resourceTable = shadowAssetManager.getResourceTable(); 87 InputStream inputStream = resourceTable.getRawValue(id, shadowAssetManager.config); 88 if (inputStream == null) { 89 throw newNotFoundException(id); 90 } else { 91 return inputStream; 92 } 93 } 94 95 /** 96 * Since {@link AssetFileDescriptor}s are not yet supported by Robolectric, {@code null} will 97 * be returned if the resource is found. If the resource cannot be found, {@link Resources.NotFoundException} will 98 * be thrown. 99 */ 100 @Implementation(maxSdk = M) openRawResourceFd(int id)101 public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException { 102 InputStream inputStream = openRawResource(id); 103 if (!(inputStream instanceof FileInputStream)) { 104 // todo fixme 105 return null; 106 } 107 108 FileInputStream fis = (FileInputStream) inputStream; 109 try { 110 return new AssetFileDescriptor(ParcelFileDescriptor.dup(fis.getFD()), 0, fis.getChannel().size()); 111 } catch (IOException e) { 112 throw newNotFoundException(id); 113 } 114 } 115 newNotFoundException(int id)116 private Resources.NotFoundException newNotFoundException(int id) { 117 ResourceTable resourceTable = legacyShadowOf(realResourcesImpl.getAssets()).getResourceTable(); 118 ResName resName = resourceTable.getResName(id); 119 if (resName == null) { 120 return new Resources.NotFoundException("resource ID #0x" + Integer.toHexString(id)); 121 } else { 122 return new Resources.NotFoundException(resName.getFullyQualifiedName()); 123 } 124 } 125 126 @HiddenApi @Implementation(maxSdk = M) loadXmlResourceParser(int resId, String type)127 public XmlResourceParser loadXmlResourceParser(int resId, String type) throws Resources.NotFoundException { 128 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResourcesImpl.getAssets()); 129 return shadowAssetManager.loadXmlResourceParser(resId, type); 130 } 131 132 @HiddenApi @Implementation loadXmlResourceParser(String file, int id, int assetCookie, String type)133 public XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws Resources.NotFoundException { 134 return loadXmlResourceParser(id, type); 135 } 136 137 @Implements(value = ResourcesImpl.ThemeImpl.class, minSdk = N, isInAndroidSdk = false, 138 shadowPicker = ShadowResourcesImpl.ShadowThemeImpl.Picker.class) 139 public static class ShadowLegacyThemeImpl extends ShadowThemeImpl { 140 @RealObject ResourcesImpl.ThemeImpl realThemeImpl; 141 142 @Implementation obtainStyledAttributes(Resources.Theme wrapper, AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)143 public TypedArray obtainStyledAttributes(Resources.Theme wrapper, AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { 144 Resources resources = wrapper.getResources(); 145 AssetManager assets = resources.getAssets(); 146 return legacyShadowOf(assets) 147 .attrsToTypedArray(resources, set, attrs, defStyleAttr, getNativePtr(), defStyleRes); 148 } 149 getNativePtr()150 public long getNativePtr() { 151 return ReflectionHelpers.getField(realThemeImpl, "mTheme"); 152 } 153 } 154 155 @Implementation(maxSdk = N_MR1) loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache)156 public Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws Resources.NotFoundException { 157 Drawable drawable = directlyOn(realResourcesImpl, ResourcesImpl.class, "loadDrawable", 158 from(Resources.class, wrapper), 159 from(TypedValue.class, value), 160 from(int.class, id), 161 from(Resources.Theme.class, theme), 162 from(boolean.class, useCache) 163 ); 164 165 ShadowResources.setCreatedFromResId(wrapper, id, drawable); 166 return drawable; 167 } 168 169 @Implementation(minSdk = O) loadDrawable(Resources wrapper, TypedValue value, int id, int density, Resources.Theme theme)170 public Drawable loadDrawable(Resources wrapper, TypedValue value, int id, int density, Resources.Theme theme) { 171 Drawable drawable = directlyOn(realResourcesImpl, ResourcesImpl.class, "loadDrawable", 172 from(Resources.class, wrapper), 173 from(TypedValue.class, value), 174 from(int.class, id), 175 from(int.class, density), 176 from(Resources.Theme.class, theme)); 177 178 ShadowResources.setCreatedFromResId(wrapper, id, drawable); 179 return drawable; 180 } 181 } 182