blob: b184eb2a49e45a4da6e3b9c5e04fe5288ec4abc5 [file] [log] [blame]
package com.android.settings.testutils.shadow;
import static android.util.TypedValue.TYPE_REFERENCE;
import static org.robolectric.RuntimeEnvironment.application;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import android.annotation.DimenRes;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.ArrayRes;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import com.android.settings.R;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.XmlResourceParserImpl;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.res.StyleData;
import org.robolectric.res.StyleResolver;
import org.robolectric.res.ThemeStyleSet;
import org.robolectric.shadows.ShadowAssetManager;
import org.robolectric.shadows.ShadowResources;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.w3c.dom.Node;
import java.util.List;
import java.util.Map;
/**
* Shadow Resources and Theme classes to handle resource references that Robolectric shadows cannot
* handle because they are too new or private.
*/
@Implements(value = Resources.class, inheritImplementationMethods = true)
public class SettingsShadowResources extends ShadowResources {
@RealObject
public Resources realResources;
private static SparseArray<Object> sResourceOverrides = new SparseArray<>();
public static void overrideResource(int id, Object value) {
synchronized (sResourceOverrides) {
sResourceOverrides.put(id, value);
}
}
public static void overrideResource(String name, Object value) {
final Resources res = application.getResources();
final int resId = res.getIdentifier(name, null, null);
if (resId == 0) {
throw new Resources.NotFoundException("Cannot override \"" + name + "\"");
}
overrideResource(resId, value);
}
public static void reset() {
synchronized (sResourceOverrides) {
sResourceOverrides.clear();
}
}
@Implementation
public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
// Handle requests for private dimension resources,
// TODO: Consider making a set of private dimension resource ids if this happens repeatedly.
if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) {
return 0;
}
return directlyOn(realResources, Resources.class).getDimensionPixelSize(id);
}
@Implementation
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
if (id == R.color.battery_icon_color_error) {
return Color.WHITE;
}
return directlyOn(realResources, Resources.class).getColor(id, theme);
}
@Implementation
public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme)
throws NotFoundException {
if (id == com.android.internal.R.color.text_color_primary) {
return ColorStateList.valueOf(Color.WHITE);
}
return directlyOn(realResources, Resources.class).getColorStateList(id, theme);
}
/**
* Deprecated because SDK 24+ uses
* {@link SettingsShadowResourcesImpl#loadDrawable(Resources, TypedValue, int, int, Theme)}
*
* TODO: Delete when all tests have been migrated to sdk 26
*/
@Deprecated
@Implementation
public Drawable loadDrawable(TypedValue value, int id, Theme theme)
throws NotFoundException {
// The drawable item in switchbar_background.xml refers to a very recent color attribute
// that Robolectric isn't yet aware of.
// TODO: Remove this once Robolectric is updated.
if (id == R.drawable.switchbar_background) {
return new ColorDrawable();
} else if (id == R.drawable.ic_launcher_settings) {
// ic_launcher_settings uses adaptive-icon, which is not supported by robolectric,
// change it to a normal drawable.
id = R.drawable.ic_settings_wireless;
} else if (id == R.drawable.app_filter_spinner_background) {
id = R.drawable.ic_expand_more_inverse;
}
return super.loadDrawable(value, id, theme);
}
@Implementation
public int[] getIntArray(@ArrayRes int id) throws NotFoundException {
// The Robolectric isn't aware of resources in settingslib, so we need to stub it here
if (id == com.android.settings.R.array.batterymeter_bolt_points
|| id == com.android.settings.R.array.batterymeter_plus_points) {
return new int[2];
}
final Object override;
synchronized (sResourceOverrides) {
override = sResourceOverrides.get(id);
}
if (override instanceof int[]) {
return (int[]) override;
}
return directlyOn(realResources, Resources.class).getIntArray(id);
}
@Implementation
public String getString(int id) {
final Object override;
synchronized (sResourceOverrides) {
override = sResourceOverrides.get(id);
}
if (override instanceof String) {
return (String) override;
}
return directlyOn(
realResources, Resources.class, "getString", ClassParameter.from(int.class, id));
}
@Implementation
public int getInteger(int id) {
final Object override;
synchronized (sResourceOverrides) {
override = sResourceOverrides.get(id);
}
if (override instanceof Integer) {
return (Integer) override;
}
return directlyOn(
realResources, Resources.class, "getInteger", ClassParameter.from(int.class, id));
}
@Implementation
public boolean getBoolean(int id) {
final Object override;
synchronized (sResourceOverrides) {
override = sResourceOverrides.get(id);
}
if (override instanceof Boolean) {
return (boolean) override;
}
return directlyOn(realResources, Resources.class, "getBoolean",
ClassParameter.from(int.class, id));
}
@Implements(value = Theme.class, inheritImplementationMethods = true)
public static class SettingsShadowTheme extends ShadowTheme {
@RealObject
Theme realTheme;
private ShadowAssetManager mAssetManager = shadowOf(
RuntimeEnvironment.application.getAssets());
@Implementation
public TypedArray obtainStyledAttributes(
AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
// Replace all private string references with a placeholder.
if (set != null) {
synchronized (set) {
for (int i = 0; i < set.getAttributeCount(); ++i) {
final String attributeValue = set.getAttributeValue(i);
final Node node = ReflectionHelpers.callInstanceMethod(
XmlResourceParserImpl.class, set, "getAttributeAt",
ReflectionHelpers.ClassParameter.from(int.class, i));
if (attributeValue.contains("attr/fingerprint_layout_theme")) {
// Workaround for https://github.com/robolectric/robolectric/issues/2641
node.setNodeValue("@style/FingerprintLayoutTheme");
} else if (attributeValue.startsWith("@*android:string")) {
node.setNodeValue("PLACEHOLDER");
}
}
}
}
// Track down all styles and remove all inheritance from private styles.
final Map<Long, Object /* NativeTheme */> appliedStylesList =
ReflectionHelpers.getField(mAssetManager, "nativeThemes");
synchronized (appliedStylesList) {
for (Long idx : appliedStylesList.keySet()) {
final ThemeStyleSet appliedStyles = ReflectionHelpers.getField(
appliedStylesList.get(idx), "themeStyleSet");
// The Object's below are actually ShadowAssetManager.OverlayedStyle.
// We can't use
// it here because it's private.
final List<Object /* OverlayedStyle */> overlayedStyles =
ReflectionHelpers.getField(appliedStyles, "styles");
for (Object appliedStyle : overlayedStyles) {
final StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle,
"style");
final List<StyleData> styleDatas =
ReflectionHelpers.getField(styleResolver, "styles");
for (StyleData styleData : styleDatas) {
if (styleData.getParent() != null &&
styleData.getParent().startsWith("@*android:style")) {
ReflectionHelpers.setField(StyleData.class, styleData, "parent",
null);
}
}
}
}
}
return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
}
@Implementation
public synchronized boolean resolveAttribute(int resid, TypedValue outValue,
boolean resolveRefs) {
// The real Resources instance in Robolectric tests somehow fails to find the
// preferenceTheme attribute in the layout. Let's do it ourselves.
if (getResources().getResourceName(resid)
.equals("com.android.settings:attr/preferenceTheme")) {
final int preferenceThemeResId =
getResources().getIdentifier(
"PreferenceTheme", "style", "com.android.settings");
outValue.type = TYPE_REFERENCE;
outValue.data = preferenceThemeResId;
outValue.resourceId = preferenceThemeResId;
return true;
}
return directlyOn(realTheme, Theme.class)
.resolveAttribute(resid, outValue, resolveRefs);
}
private Resources getResources() {
return ReflectionHelpers.callInstanceMethod(ShadowTheme.class, this, "getResources");
}
}
}