| package org.robolectric.shadows; |
| |
| import android.content.res.AssetFileDescriptor; |
| import android.content.res.AssetManager; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.content.res.XmlResourceParser; |
| import android.os.ParcelFileDescriptor; |
| import android.util.AttributeSet; |
| import android.util.TypedValue; |
| |
| import org.jetbrains.annotations.NotNull; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.HiddenApi; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.annotation.RealObject; |
| import org.robolectric.res.ThemeStyleSet; |
| import org.robolectric.res.AttributeResource; |
| import org.robolectric.res.DrawableNode; |
| import org.robolectric.res.DrawableResourceLoader; |
| import org.robolectric.res.FsFile; |
| import org.robolectric.res.ResName; |
| import org.robolectric.res.ResType; |
| import org.robolectric.res.ResourceIndex; |
| import org.robolectric.res.ResourceLoader; |
| import org.robolectric.res.Style; |
| import org.robolectric.res.StyleData; |
| import org.robolectric.res.StyleResolver; |
| import org.robolectric.res.TypedResource; |
| import org.robolectric.res.builder.ResourceParser; |
| import org.robolectric.res.builder.XmlResourceParserImpl; |
| import org.robolectric.util.ReflectionHelpers; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import static org.robolectric.Shadows.shadowOf; |
| |
| /** |
| * Shadow for {@link android.content.res.AssetManager}. |
| */ |
| @Implements(AssetManager.class) |
| public final class ShadowAssetManager { |
| public static final int STYLE_NUM_ENTRIES = 6; |
| public static final int STYLE_TYPE = 0; |
| public static final int STYLE_DATA = 1; |
| public static final int STYLE_ASSET_COOKIE = 2; |
| public static final int STYLE_RESOURCE_ID = 3; |
| public static final int STYLE_CHANGING_CONFIGURATIONS = 4; |
| public static final int STYLE_DENSITY = 5; |
| |
| private Map<Number, ThemeStyleSet> themes = |
| Collections.synchronizedMap(new HashMap<Number, ThemeStyleSet>()); |
| private $ptrClass nextInternalThemeId = 1000; |
| private ResourceLoader resourceLoader; |
| |
| @RealObject |
| AssetManager realObject; |
| |
| public ResourceLoader getResourceLoader() { |
| return resourceLoader; |
| } |
| |
| public void __constructor__() { |
| resourceLoader = RuntimeEnvironment.getAppResourceLoader(); |
| } |
| |
| public void __constructor__(boolean isSystem) { |
| resourceLoader = isSystem ? RuntimeEnvironment.getSystemResourceLoader() : RuntimeEnvironment.getAppResourceLoader(); |
| } |
| |
| @HiddenApi @Implementation |
| public CharSequence getResourceText(int ident) { |
| TypedResource value = getAndResolve(ident, RuntimeEnvironment.getQualifiers(), true); |
| if (value == null) return null; |
| return (CharSequence) value.getData(); |
| } |
| |
| @HiddenApi @Implementation |
| public CharSequence getResourceBagText(int ident, int bagEntryId) { |
| throw new UnsupportedOperationException(); // todo |
| } |
| |
| @HiddenApi @Implementation |
| public String[] getResourceStringArray(final int id) { |
| CharSequence[] resourceTextArray = getResourceTextArray(id); |
| if (resourceTextArray == null) return null; |
| String[] strings = new String[resourceTextArray.length]; |
| for (int i = 0; i < strings.length; i++) { |
| strings[i] = resourceTextArray[i].toString(); |
| } |
| return strings; |
| } |
| |
| @HiddenApi @Implementation |
| public int getResourceIdentifier(String name, String defType, String defPackage) { |
| TypedResource value = resourceLoader.getValue(ResName.qualifyResName(name, defPackage, defType), RuntimeEnvironment.getQualifiers()); |
| if (value == null) { |
| return 0; |
| } |
| ResourceIndex resourceIndex = resourceLoader.getResourceIndex(); |
| ResName resName = ResName.qualifyResName(name, defPackage, defType); |
| return resourceIndex.getResourceId(resName); |
| } |
| |
| @HiddenApi @Implementation |
| public boolean getResourceValue(int ident, int density, TypedValue outValue, boolean resolveRefs) { |
| TypedResource value = getAndResolve(ident, RuntimeEnvironment.getQualifiers(), resolveRefs); |
| if (value == null) return false; |
| |
| getConverter(value).fillTypedValue(value.getData(), outValue); |
| return true; |
| } |
| |
| private Converter getConverter(TypedResource value) { |
| return Converter.getConverter(value.getResType()); |
| } |
| |
| @HiddenApi @Implementation |
| public CharSequence[] getResourceTextArray(int resId) { |
| TypedResource value = getAndResolve(resId, RuntimeEnvironment.getQualifiers(), true); |
| if (value == null) return null; |
| TypedResource[] items = getConverter(value).getItems(value); |
| CharSequence[] charSequences = new CharSequence[items.length]; |
| for (int i = 0; i < items.length; i++) { |
| TypedResource typedResource = resolve(items[i], RuntimeEnvironment.getQualifiers(), resId); |
| charSequences[i] = getConverter(typedResource).asCharSequence(typedResource); |
| } |
| return charSequences; |
| } |
| |
| @HiddenApi @Implementation |
| public boolean getThemeValue($ptrClass themePtr, int ident, TypedValue outValue, boolean resolveRefs) { |
| ResourceIndex resourceIndex = resourceLoader.getResourceIndex(); |
| ResName resName = resourceIndex.getResName(ident); |
| |
| ThemeStyleSet themeStyleSet = getThemeStyleSet(themePtr); |
| AttributeResource attrValue = themeStyleSet.getAttrValue(resName); |
| while(resolveRefs && attrValue != null && attrValue.isStyleReference()) { |
| ResName attrResName = new ResName(attrValue.contextPackageName, "attr", attrValue.value.substring(1)); |
| attrValue = themeStyleSet.getAttrValue(attrResName); |
| } |
| if (attrValue != null) { |
| Converter.convertAndFill(attrValue, outValue, resourceLoader, RuntimeEnvironment.getQualifiers(), resolveRefs); |
| return true; |
| } |
| return false; |
| } |
| |
| @HiddenApi @Implementation |
| public void ensureStringBlocks() { |
| } |
| |
| @Implementation |
| public final InputStream open(String fileName) throws IOException { |
| return ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(fileName).getInputStream(); |
| } |
| |
| @Implementation |
| public final InputStream open(String fileName, int accessMode) throws IOException { |
| return ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(fileName).getInputStream(); |
| } |
| |
| @Implementation |
| public final AssetFileDescriptor openFd(String fileName) throws IOException { |
| File file = new File(ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(fileName).getPath()); |
| ParcelFileDescriptor parcelFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); |
| return new AssetFileDescriptor(parcelFileDescriptor, 0, file.length()); |
| } |
| |
| @Implementation |
| public final String[] list(String path) throws IOException { |
| FsFile file = ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(path); |
| if (file.isDirectory()) { |
| return file.listFileNames(); |
| } |
| return new String[0]; |
| } |
| |
| @HiddenApi @Implementation |
| public final InputStream openNonAsset(int cookie, String fileName, int accessMode) throws IOException { |
| final ResName resName = qualifyFromNonAssetFileName(fileName); |
| final DrawableNode drawableNode = resourceLoader.getDrawableNode(resName, RuntimeEnvironment.getQualifiers()); |
| |
| if (drawableNode == null) { |
| throw new IOException("Unable to find resource for " + fileName); |
| } |
| |
| if (accessMode == AssetManager.ACCESS_STREAMING) { |
| return drawableNode.getFsFile().getInputStream(); |
| } else { |
| return new ByteArrayInputStream(drawableNode.getFsFile().getBytes()); |
| } |
| } |
| |
| private ResName qualifyFromNonAssetFileName(String fileName) { |
| if (fileName.startsWith("jar:")) { |
| // Must remove "jar:" prefix, or else qualifyFromFilePath fails on Windows |
| return ResName.qualifyFromFilePath("android", fileName.replaceFirst("jar:", "")); |
| } else { |
| return ResName.qualifyFromFilePath(ShadowApplication.getInstance().getAppManifest().getPackageName(), fileName); |
| } |
| } |
| |
| @HiddenApi @Implementation |
| public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Implementation |
| public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException { |
| return ResourceParser.create(fileName, "fixme", "fixme", null); |
| } |
| |
| @HiddenApi @Implementation |
| public int addAssetPath(String path) { |
| return 1; |
| } |
| |
| @HiddenApi @Implementation |
| public boolean isUpToDate() { |
| return true; |
| } |
| |
| @HiddenApi @Implementation |
| public void setLocale(String locale) { |
| } |
| |
| @Implementation |
| public String[] getLocales() { |
| return new String[0]; // todo |
| } |
| |
| @HiddenApi @Implementation |
| public void setConfiguration(int mcc, int mnc, String locale, |
| int orientation, int touchscreen, int density, int keyboard, |
| int keyboardHidden, int navigation, int screenWidth, int screenHeight, |
| int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, |
| int screenLayout, int uiMode, int majorVersion) { |
| } |
| |
| @HiddenApi @Implementation |
| public int[] getArrayIntResource(int resId) { |
| TypedResource value = getAndResolve(resId, RuntimeEnvironment.getQualifiers(), true); |
| if (value == null) return null; |
| TypedResource[] items = getConverter(value).getItems(value); |
| int[] ints = new int[items.length]; |
| for (int i = 0; i < items.length; i++) { |
| TypedResource typedResource = resolve(items[i], RuntimeEnvironment.getQualifiers(), resId); |
| ints[i] = getConverter(typedResource).asInt(typedResource); |
| } |
| return ints; |
| } |
| |
| @HiddenApi @Implementation |
| synchronized public $ptrClass createTheme() { |
| return nextInternalThemeId++; |
| } |
| |
| @HiddenApi @Implementation |
| public void releaseTheme($ptrClass themePtr) { |
| themes.remove(themePtr); |
| } |
| |
| @HiddenApi @Implementation |
| public static void applyThemeStyle($ptrClass themePtr, int styleRes, boolean force) { |
| ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets()); |
| |
| final Map<Number, ThemeStyleSet> themes = assetManager.themes; |
| |
| ThemeStyleSet themeStyleSet = themes.get(themePtr); |
| if (themeStyleSet == null) { |
| themeStyleSet = new ThemeStyleSet(); |
| themes.put(themePtr, themeStyleSet); |
| } |
| |
| Style style = assetManager.resolveStyle(styleRes, null); |
| |
| themeStyleSet.apply(style, force); |
| } |
| |
| @HiddenApi @Implementation |
| public static void copyTheme($ptrClass destPtr, $ptrClass sourcePtr) { |
| ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets()); |
| assetManager.themes.put(destPtr, assetManager.getThemeStyleSet(sourcePtr).copy()); |
| } |
| |
| ///////////////////////// |
| |
| private Style resolveStyle(int resId, Style themeStyleSet) { |
| return resolveStyle(getResName(resId), themeStyleSet); |
| } |
| |
| private Style resolveStyle(@NotNull ResName themeStyleName, Style themeStyleSet) { |
| TypedResource themeStyleResource = resourceLoader.getValue(themeStyleName, RuntimeEnvironment.getQualifiers()); |
| if (themeStyleResource == null) return null; |
| StyleData themeStyleData = (StyleData) themeStyleResource.getData(); |
| if (themeStyleSet == null) { |
| themeStyleSet = new ThemeStyleSet(); |
| } |
| return new StyleResolver(resourceLoader, themeStyleData, themeStyleSet, themeStyleName, RuntimeEnvironment.getQualifiers()); |
| } |
| |
| private TypedResource getAndResolve(int resId, String qualifiers, boolean resolveRefs) { |
| TypedResource value = resourceLoader.getValue(resId, qualifiers); |
| if (resolveRefs) { |
| value = resolve(value, qualifiers, resId); |
| } |
| |
| // todo: make the drawable loader put stuff into the normal spot... |
| String resourceTypeName = getResourceTypeName(resId); |
| if (value == null && DrawableResourceLoader.isStillHandledHere(resourceTypeName)) { |
| DrawableNode drawableNode = resourceLoader.getDrawableNode(resId, qualifiers); |
| return new TypedResource<>(drawableNode.getFsFile(), ResType.FILE); |
| } |
| |
| // todo: gross. this is so resources.getString(R.layout.foo) works for ABS. |
| if (value == null && "layout".equals(resourceTypeName)) { |
| throw new UnsupportedOperationException("ugh, this doesn't work still?"); |
| } |
| |
| return value; |
| } |
| |
| TypedResource resolve(TypedResource value, String qualifiers, int resId) { |
| return resourceLoader.resolveResourceValue(value, qualifiers, resId); |
| } |
| |
| private AttributeResource buildAttribute(AttributeSet set, int resId, int defStyleAttr, $ptrClass themePtr, int defStyleRes) { |
| /* |
| * When determining the final value of a particular attribute, there are four inputs that come into play: |
| * |
| * 1. Any attribute values in the given AttributeSet. |
| * 2. The style resource specified in the AttributeSet (named "style"). |
| * 3. The default style specified by defStyleAttr and defStyleRes |
| * 4. The base values in this theme. |
| */ |
| Style defStyleFromAttr = null; |
| Style defStyleFromRes = null; |
| Style styleAttrStyle = null; |
| |
| ThemeStyleSet themeStyleSet = getThemeStyleSet(themePtr); |
| if (themeStyleSet == null) { |
| themeStyleSet = new ThemeStyleSet(); |
| } |
| if (defStyleAttr != 0) { |
| // Load the theme attribute for the default style attributes. E.g., attr/buttonStyle |
| ResName defStyleName = getResName(defStyleAttr); |
| |
| // Load the style for the default style attribute. E.g. "@style/Widget.Robolectric.Button"; |
| AttributeResource defStyleAttribute = themeStyleSet.getAttrValue(defStyleName); |
| if (defStyleAttribute != null) { |
| while (defStyleAttribute.isStyleReference()) { |
| AttributeResource other = themeStyleSet.getAttrValue(defStyleAttribute.getStyleReference()); |
| if (other == null) { |
| throw new RuntimeException("couldn't dereference " + defStyleAttribute); |
| } |
| defStyleAttribute = other; |
| } |
| |
| if (defStyleAttribute.isResourceReference()) { |
| ResName defStyleResName = defStyleAttribute.getResourceReference(); |
| defStyleFromAttr = resolveStyle(defStyleResName, themeStyleSet); |
| } |
| } |
| } |
| |
| if (set != null && set.getStyleAttribute() != 0) { |
| ResName styleAttributeResName = getResName(set.getStyleAttribute()); |
| while (styleAttributeResName.type.equals("attr")) { |
| AttributeResource attrValue = themeStyleSet.getAttrValue(styleAttributeResName); |
| if (attrValue == null) { |
| throw new RuntimeException( |
| "no value for " + styleAttributeResName.getFullyQualifiedName() |
| + " in " + themeStyleSet); |
| } |
| if (attrValue.isResourceReference()) { |
| styleAttributeResName = attrValue.getResourceReference(); |
| } else if (attrValue.isStyleReference()) { |
| styleAttributeResName = attrValue.getStyleReference(); |
| } |
| } |
| styleAttrStyle = resolveStyle(styleAttributeResName, themeStyleSet); |
| } |
| |
| if (defStyleRes != 0) { |
| ResName resName = getResName(defStyleRes); |
| if (resName.type.equals("attr")) { |
| AttributeResource attributeValue = findAttributeValue(defStyleRes, set, styleAttrStyle, defStyleFromAttr, defStyleFromAttr, themeStyleSet); |
| if (attributeValue != null) { |
| if (attributeValue.isStyleReference()) { |
| resName = themeStyleSet.getAttrValue(attributeValue.getStyleReference()).getResourceReference(); |
| } else if (attributeValue.isResourceReference()) { |
| resName = attributeValue.getResourceReference(); |
| } |
| } |
| } |
| defStyleFromRes = resolveStyle(resName, themeStyleSet); |
| } |
| |
| AttributeResource attribute = findAttributeValue(resId, set, styleAttrStyle, defStyleFromAttr, defStyleFromRes, themeStyleSet); |
| while (attribute != null && attribute.isStyleReference()) { |
| ResName otherAttrName = attribute.getStyleReference(); |
| |
| attribute = themeStyleSet.getAttrValue(otherAttrName); |
| if (attribute != null) { |
| attribute = new AttributeResource(resourceLoader.getResourceIndex().getResName(resId), attribute.value, attribute.contextPackageName); |
| } |
| } |
| |
| return attribute; |
| } |
| |
| private ThemeStyleSet getThemeStyleSet($ptrClass themePtr) { |
| ThemeStyleSet themeStyleSet = themes.get(themePtr); |
| return themeStyleSet == null ? new ThemeStyleSet() : themeStyleSet; |
| } |
| |
| TypedArray attrsToTypedArray(Resources resources, AttributeSet set, int[] attrs, int defStyleAttr, Resources.Theme theme, int defStyleRes) { |
| $ptrClass themePtr = ReflectionHelpers.getField(theme, "mTheme"); |
| return attrsToTypedArray(resources, set, attrs, defStyleAttr, themePtr, defStyleRes); |
| } |
| |
| TypedArray attrsToTypedArray(Resources resources, AttributeSet set, int[] attrs, int defStyleAttr, $ptrClass themePtr, int defStyleRes) { |
| CharSequence[] stringData = new CharSequence[attrs.length]; |
| int[] data = new int[attrs.length * ShadowAssetManager.STYLE_NUM_ENTRIES]; |
| int[] indices = new int[attrs.length + 1]; |
| int nextIndex = 0; |
| |
| for (int i = 0; i < attrs.length; i++) { |
| int offset = i * ShadowAssetManager.STYLE_NUM_ENTRIES; |
| |
| AttributeResource attribute = buildAttribute(set, attrs[i], defStyleAttr, themePtr, defStyleRes); |
| if (attribute != null && !attribute.isNull()) { |
| TypedValue typedValue = new TypedValue(); |
| // If there is an AttributeSet then use the resource loader that the attribute set was created with. |
| ResourceLoader resourceLoader = set != null && set instanceof XmlResourceParserImpl |
| ? ((XmlResourceParserImpl) set).getResourceLoader() |
| : this.resourceLoader; |
| Converter.convertAndFill(attribute, typedValue, resourceLoader, RuntimeEnvironment.getQualifiers(), true); |
| //noinspection PointlessArithmeticExpression |
| data[offset + ShadowAssetManager.STYLE_TYPE] = typedValue.type; |
| data[offset + ShadowAssetManager.STYLE_DATA] = typedValue.type == TypedValue.TYPE_STRING ? i : typedValue.data; |
| data[offset + ShadowAssetManager.STYLE_ASSET_COOKIE] = typedValue.assetCookie; |
| data[offset + ShadowAssetManager.STYLE_RESOURCE_ID] = typedValue.resourceId; |
| data[offset + ShadowAssetManager.STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations; |
| data[offset + ShadowAssetManager.STYLE_DENSITY] = typedValue.density; |
| stringData[i] = typedValue.string; |
| |
| indices[nextIndex + 1] = i; |
| nextIndex++; |
| } |
| } |
| |
| indices[0] = nextIndex; |
| |
| TypedArray typedArray = ShadowTypedArray.create(resources, attrs, data, indices, nextIndex, stringData); |
| if (set != null) { |
| shadowOf(typedArray).positionDescription = set.getPositionDescription(); |
| } |
| return typedArray; |
| } |
| |
| private AttributeResource findAttributeValue(int resId, AttributeSet attributeSet, Style styleAttrStyle, Style defStyleFromAttr, Style defStyleFromRes, @NotNull ThemeStyleSet themeStyleSet) { |
| if (attributeSet != null) { |
| for (int i = 0; i < attributeSet.getAttributeCount(); i++) { |
| if (attributeSet.getAttributeNameResource(i) == resId && attributeSet.getAttributeValue(i) != null) { |
| ResName resName = resourceLoader.getResourceIndex().getResName(resId); |
| |
| if (resName == null) { |
| // We're looking for an attribute that doesn't yet exist at this SDK level just pass it back blindly and |
| // assume the user knows what they're doing with it. |
| resName = ResName.qualifyResName(attributeSet.getAttributeName(i), null, "attr"); |
| } |
| return new AttributeResource(resName, attributeSet.getAttributeValue(i), "fixme!!!"); |
| } |
| } |
| } |
| |
| ResName attrName = resourceLoader.getResourceIndex().getResName(resId); |
| if (attrName == null) return null; |
| |
| if (styleAttrStyle != null) { |
| AttributeResource attribute = styleAttrStyle.getAttrValue(attrName); |
| if (attribute != null) { |
| return attribute; |
| } |
| } |
| |
| // else if attr in defStyleFromAttr, use its value |
| if (defStyleFromAttr != null) { |
| AttributeResource attribute = defStyleFromAttr.getAttrValue(attrName); |
| if (attribute != null) { |
| return attribute; |
| } |
| } |
| |
| if (defStyleFromRes != null) { |
| AttributeResource attribute = defStyleFromRes.getAttrValue(attrName); |
| if (attribute != null) { |
| return attribute; |
| } |
| } |
| |
| // else if attr in theme, use its value |
| return themeStyleSet.getAttrValue(attrName); |
| } |
| |
| @NotNull private ResName getResName(int id) { |
| ResName resName = resourceLoader.getResourceIndex().getResName(id); |
| return checkResName(id, resName); |
| } |
| |
| private ResName checkResName(int id, ResName resName) { |
| if (resName == null) { |
| throw new Resources.NotFoundException("Unable to find resource ID #0x" + Integer.toHexString(id)); |
| } |
| return resName; |
| } |
| |
| @Implementation |
| public String getResourceName(int resid) { |
| return getResName(resid).getFullyQualifiedName(); |
| } |
| |
| @Implementation |
| public String getResourcePackageName(int resid) { |
| return getResName(resid).packageName; |
| } |
| |
| @Implementation |
| public String getResourceTypeName(int resid) { |
| return getResName(resid).type; |
| } |
| |
| @Implementation |
| public String getResourceEntryName(int resid) { |
| return getResName(resid).name; |
| } |
| } |