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.LongSparseArray; 21 import android.util.TypedValue; 22 import java.io.FileInputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.lang.reflect.Field; 26 import java.lang.reflect.Modifier; 27 import java.util.ArrayList; 28 import java.util.List; 29 import java.util.Locale; 30 import org.robolectric.RuntimeEnvironment; 31 import org.robolectric.annotation.HiddenApi; 32 import org.robolectric.annotation.Implementation; 33 import org.robolectric.annotation.Implements; 34 import org.robolectric.annotation.RealObject; 35 import org.robolectric.annotation.Resetter; 36 import org.robolectric.res.Plural; 37 import org.robolectric.res.PluralRules; 38 import org.robolectric.res.ResName; 39 import org.robolectric.res.ResType; 40 import org.robolectric.res.ResourceTable; 41 import org.robolectric.res.TypedResource; 42 import org.robolectric.shadows.ShadowResourcesImpl.Picker; 43 import org.robolectric.util.ReflectionHelpers; 44 import org.robolectric.util.ReflectionHelpers.ClassParameter; 45 46 @SuppressWarnings("NewApi") 47 @Implements(value = ResourcesImpl.class, isInAndroidSdk = false, minSdk = N, 48 shadowPicker = Picker.class) 49 public class ShadowArscResourcesImpl extends ShadowResourcesImpl { 50 private static List<LongSparseArray<?>> resettableArrays; 51 52 @RealObject 53 ResourcesImpl realResourcesImpl; 54 55 @Resetter reset()56 public static void reset() { 57 if (RuntimeEnvironment.useLegacyResources()) { 58 ShadowResourcesImpl.reset(); 59 } 60 } 61 obtainResettableArrays()62 private static List<LongSparseArray<?>> obtainResettableArrays() { 63 List<LongSparseArray<?>> resettableArrays = new ArrayList<>(); 64 Field[] allFields = Resources.class.getDeclaredFields(); 65 for (Field field : allFields) { 66 if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(LongSparseArray.class)) { 67 field.setAccessible(true); 68 try { 69 LongSparseArray<?> longSparseArray = (LongSparseArray<?>) field.get(null); 70 if (longSparseArray != null) { 71 resettableArrays.add(longSparseArray); 72 } 73 } catch (IllegalAccessException e) { 74 throw new RuntimeException(e); 75 } 76 } 77 } 78 return resettableArrays; 79 } 80 81 @Implementation(maxSdk = M) getQuantityString(int id, int quantity, Object... formatArgs)82 public String getQuantityString(int id, int quantity, Object... formatArgs) throws Resources.NotFoundException { 83 String raw = getQuantityString(id, quantity); 84 return String.format(Locale.ENGLISH, raw, formatArgs); 85 } 86 87 @Implementation(maxSdk = M) getQuantityString(int resId, int quantity)88 public String getQuantityString(int resId, int quantity) throws Resources.NotFoundException { 89 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResourcesImpl.getAssets()); 90 91 TypedResource typedResource = shadowAssetManager.getResourceTable().getValue(resId, shadowAssetManager.config); 92 if (typedResource != null && typedResource instanceof PluralRules) { 93 PluralRules pluralRules = (PluralRules) typedResource; 94 Plural plural = pluralRules.find(quantity); 95 96 if (plural == null) { 97 return null; 98 } 99 100 TypedResource<?> resolvedTypedResource = shadowAssetManager.resolve( 101 new TypedResource<>(plural.getString(), ResType.CHAR_SEQUENCE, pluralRules.getXmlContext()), shadowAssetManager.config, resId); 102 return resolvedTypedResource == null ? null : resolvedTypedResource.asString(); 103 } else { 104 return null; 105 } 106 } 107 108 @Implementation(maxSdk = M) openRawResource(int id)109 public InputStream openRawResource(int id) throws Resources.NotFoundException { 110 if (false) { 111 ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResourcesImpl.getAssets()); 112 ResourceTable resourceTable = shadowAssetManager.getResourceTable(); 113 InputStream inputStream = resourceTable.getRawValue(id, shadowAssetManager.config); 114 if (inputStream == null) { 115 throw newNotFoundException(id); 116 } else { 117 return inputStream; 118 } 119 } else { 120 return directlyOn(realResourcesImpl, ResourcesImpl.class, "openRawResource", 121 from(int.class, id)); 122 } 123 } 124 125 /** 126 * Since {@link AssetFileDescriptor}s are not yet supported by Robolectric, {@code null} will 127 * be returned if the resource is found. If the resource cannot be found, {@link Resources.NotFoundException} will 128 * be thrown. 129 */ 130 @Implementation(maxSdk = M) openRawResourceFd(int id)131 public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException { 132 InputStream inputStream = openRawResource(id); 133 if (!(inputStream instanceof FileInputStream)) { 134 // todo fixme 135 return null; 136 } 137 138 FileInputStream fis = (FileInputStream) inputStream; 139 try { 140 return new AssetFileDescriptor(ParcelFileDescriptor.dup(fis.getFD()), 0, fis.getChannel().size()); 141 } catch (IOException e) { 142 throw newNotFoundException(id); 143 } 144 } 145 newNotFoundException(int id)146 private Resources.NotFoundException newNotFoundException(int id) { 147 ResourceTable resourceTable = legacyShadowOf(realResourcesImpl.getAssets()).getResourceTable(); 148 ResName resName = resourceTable.getResName(id); 149 if (resName == null) { 150 return new Resources.NotFoundException("resource ID #0x" + Integer.toHexString(id)); 151 } else { 152 return new Resources.NotFoundException(resName.getFullyQualifiedName()); 153 } 154 } 155 156 @Implementation(maxSdk = N_MR1) loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache)157 public Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws Resources.NotFoundException { 158 Drawable drawable = directlyOn(realResourcesImpl, ResourcesImpl.class, "loadDrawable", 159 from(Resources.class, wrapper), 160 from(TypedValue.class, value), 161 from(int.class, id), 162 from(Resources.Theme.class, theme), 163 from(boolean.class, useCache) 164 ); 165 166 ShadowResources.setCreatedFromResId(wrapper, id, drawable); 167 return drawable; 168 } 169 170 @Implementation(minSdk = O) loadDrawable(Resources wrapper, TypedValue value, int id, int density, Resources.Theme theme)171 public Drawable loadDrawable(Resources wrapper, TypedValue value, int id, int density, Resources.Theme theme) { 172 Drawable drawable = directlyOn(realResourcesImpl, ResourcesImpl.class, "loadDrawable", 173 from(Resources.class, wrapper), 174 from(TypedValue.class, value), 175 from(int.class, id), 176 from(int.class, density), 177 from(Resources.Theme.class, theme)); 178 179 ShadowResources.setCreatedFromResId(wrapper, id, drawable); 180 return drawable; 181 } 182 } 183