Home | History | Annotate | Download | only in shadow
      1 package com.android.settings.testutils.shadow;
      2 
      3 import static android.util.TypedValue.TYPE_REFERENCE;
      4 
      5 import static org.robolectric.RuntimeEnvironment.application;
      6 import static org.robolectric.Shadows.shadowOf;
      7 import static org.robolectric.internal.Shadow.directlyOn;
      8 
      9 import android.annotation.DimenRes;
     10 import android.content.res.ColorStateList;
     11 import android.content.res.Resources;
     12 import android.content.res.Resources.NotFoundException;
     13 import android.content.res.Resources.Theme;
     14 import android.content.res.TypedArray;
     15 import android.graphics.Color;
     16 import android.graphics.drawable.ColorDrawable;
     17 import android.graphics.drawable.Drawable;
     18 import android.support.annotation.ArrayRes;
     19 import android.support.annotation.ColorRes;
     20 import android.support.annotation.Nullable;
     21 import android.util.AttributeSet;
     22 import android.util.SparseArray;
     23 import android.util.TypedValue;
     24 
     25 import com.android.settings.R;
     26 
     27 import org.robolectric.RuntimeEnvironment;
     28 import org.robolectric.annotation.Implementation;
     29 import org.robolectric.annotation.Implements;
     30 import org.robolectric.annotation.RealObject;
     31 import org.robolectric.internal.Shadow;
     32 import org.robolectric.res.StyleData;
     33 import org.robolectric.res.StyleResolver;
     34 import org.robolectric.res.builder.XmlResourceParserImpl;
     35 import org.robolectric.shadows.ShadowAssetManager;
     36 import org.robolectric.shadows.ShadowResources;
     37 import org.robolectric.util.ReflectionHelpers;
     38 import org.robolectric.util.ReflectionHelpers.ClassParameter;
     39 import org.w3c.dom.Node;
     40 
     41 import java.util.List;
     42 import java.util.Map;
     43 
     44 /**
     45  * Shadow Resources and Theme classes to handle resource references that Robolectric shadows cannot
     46  * handle because they are too new or private.
     47  */
     48 @Implements(Resources.class)
     49 public class SettingsShadowResources extends ShadowResources {
     50 
     51     @RealObject
     52     public Resources realResources;
     53 
     54     private static SparseArray<Object> sResourceOverrides = new SparseArray<>();
     55 
     56     public static void overrideResource(int id, Object value) {
     57         sResourceOverrides.put(id, value);
     58     }
     59 
     60     public static void overrideResource(String name, Object value) {
     61         final Resources res = application.getResources();
     62         final int resId = res.getIdentifier(name, null, null);
     63         if (resId == 0) {
     64             throw new Resources.NotFoundException("Cannot override \"" + name + "\"");
     65         }
     66         overrideResource(resId, value);
     67     }
     68 
     69     public static void reset() {
     70         sResourceOverrides.clear();
     71     }
     72 
     73     @Implementation
     74     public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
     75         // Handle requests for private dimension resources,
     76         // TODO: Consider making a set of private dimension resource ids if this happens repeatedly.
     77         if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) {
     78             return 0;
     79         }
     80         return directlyOn(realResources, Resources.class).getDimensionPixelSize(id);
     81     }
     82 
     83     @Implementation
     84     public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
     85         if (id == R.color.battery_icon_color_error) {
     86             return Color.WHITE;
     87         }
     88         return directlyOn(realResources, Resources.class).getColor(id, theme);
     89     }
     90 
     91     @Implementation
     92     public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme)
     93             throws NotFoundException {
     94         if (id == com.android.internal.R.color.text_color_primary) {
     95             return ColorStateList.valueOf(Color.WHITE);
     96         }
     97         return directlyOn(realResources, Resources.class).getColorStateList(id, theme);
     98     }
     99 
    100     @Implementation
    101     public Drawable loadDrawable(TypedValue value, int id, Theme theme)
    102             throws NotFoundException {
    103         // The drawable item in switchbar_background.xml refers to a very recent color attribute
    104         // that Robolectric isn't yet aware of.
    105         // TODO: Remove this once Robolectric is updated.
    106         if (id == R.drawable.switchbar_background) {
    107             return new ColorDrawable();
    108         } else if (id == R.drawable.ic_launcher_settings) {
    109             // ic_launcher_settings uses adaptive-icon, which is not supported by robolectric,
    110             // change it to a normal drawable.
    111             id = R.drawable.ic_settings_wireless;
    112         } else if (id == R.drawable.app_filter_spinner_background) {
    113             id = R.drawable.ic_expand_more_inverse;
    114         } else if (id == R.drawable.selectable_card_grey) {
    115             id = R.drawable.ic_expand_more_inverse;
    116         }
    117         return super.loadDrawable(value, id, theme);
    118     }
    119 
    120     @Implementation
    121     public int[] getIntArray(@ArrayRes int id) throws NotFoundException {
    122         // The Robolectric isn't aware of resources in settingslib, so we need to stub it here
    123         if (id == com.android.settings.R.array.batterymeter_bolt_points
    124                 || id == com.android.settings.R.array.batterymeter_plus_points) {
    125             return new int[2];
    126         }
    127         return directlyOn(realResources, Resources.class).getIntArray(id);
    128     }
    129 
    130     @Implementation
    131     public String getString(int id) {
    132         final Object override = sResourceOverrides.get(id);
    133         if (override instanceof String) {
    134             return (String) override;
    135         }
    136         return Shadow.directlyOn(
    137                 realResources, Resources.class, "getString", ClassParameter.from(int.class, id));
    138     }
    139 
    140     @Implementation
    141     public int getInteger(int id) {
    142         final Object override = sResourceOverrides.get(id);
    143         if (override instanceof Integer) {
    144             return (Integer) override;
    145         }
    146         return Shadow.directlyOn(
    147                 realResources, Resources.class, "getInteger", ClassParameter.from(int.class, id));
    148     }
    149 
    150     @Implementation
    151     public boolean getBoolean(int id) {
    152         final Object override = sResourceOverrides.get(id);
    153         if (override instanceof Boolean) {
    154             return (boolean) override;
    155         }
    156         return Shadow.directlyOn(realResources, Resources.class, "getBoolean",
    157                 ClassParameter.from(int.class, id));
    158     }
    159 
    160     @Implements(Theme.class)
    161     public static class SettingsShadowTheme extends ShadowTheme {
    162 
    163         @RealObject
    164         Theme realTheme;
    165 
    166         @Implementation
    167         public TypedArray obtainStyledAttributes(
    168                 AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
    169             // Replace all private string references with a placeholder.
    170             if (set != null) {
    171                 for (int i = 0; i < set.getAttributeCount(); ++i) {
    172                     String attributeValue = set.getAttributeValue(i);
    173                     Node node = ReflectionHelpers.callInstanceMethod(
    174                             XmlResourceParserImpl.class, set, "getAttributeAt",
    175                             ReflectionHelpers.ClassParameter.from(int.class, i));
    176                     if (attributeValue.contains("attr/fingerprint_layout_theme")) {
    177                         // Workaround for https://github.com/robolectric/robolectric/issues/2641
    178                         node.setNodeValue("@style/FingerprintLayoutTheme");
    179                     } else if (attributeValue.startsWith("@*android:string")) {
    180                         node.setNodeValue("PLACEHOLDER");
    181                     }
    182                 }
    183             }
    184 
    185             // Track down all styles and remove all inheritance from private styles.
    186             ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets());
    187             // The Object's below are actually ShadowAssetManager.OverlayedStyle. We can't use it
    188             // here because it's package private.
    189             Map<Long, List<Object>> appliedStylesList =
    190                     ReflectionHelpers.getField(assetManager, "appliedStyles");
    191             for (Long idx : appliedStylesList.keySet()) {
    192                 List<Object> appliedStyles = appliedStylesList.get(idx);
    193                 for (Object appliedStyle : appliedStyles) {
    194                     StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle, "style");
    195                     List<StyleData> styleDatas =
    196                             ReflectionHelpers.getField(styleResolver, "styles");
    197                     for (StyleData styleData : styleDatas) {
    198                         if (styleData.getParent() != null &&
    199                                 styleData.getParent().startsWith("@*android:style")) {
    200                             ReflectionHelpers.setField(StyleData.class, styleData, "parent", null);
    201                         }
    202                     }
    203                 }
    204 
    205             }
    206             return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
    207         }
    208 
    209         @Implementation
    210         public boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) {
    211             // The real Resources instance in Robolectric tests somehow fails to find the
    212             // preferenceTheme attribute in the layout. Let's do it ourselves.
    213             if (getResources().getResourceName(resid)
    214                     .equals("com.android.settings:attr/preferenceTheme")) {
    215                 int preferenceThemeResId =
    216                         getResources().getIdentifier(
    217                                 "PreferenceTheme", "style", "com.android.settings");
    218                 outValue.type = TYPE_REFERENCE;
    219                 outValue.data = preferenceThemeResId;
    220                 outValue.resourceId = preferenceThemeResId;
    221                 return true;
    222             }
    223             return directlyOn(realTheme, Theme.class)
    224                     .resolveAttribute(resid, outValue, resolveRefs);
    225         }
    226     }
    227 }
    228