summaryrefslogtreecommitdiffstats
path: root/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowResources.java
blob: d0c3373965b3ab2ec355914b7bd965f3f0e2d91b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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 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;

import androidx.annotation.ArrayRes;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;

/**
 * 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");
        }
    }
}