1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.core.content.res;
17 
18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
19 
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.drawable.Drawable;
25 import android.util.AttributeSet;
26 import android.util.TypedValue;
27 
28 import androidx.annotation.AnyRes;
29 import androidx.annotation.ColorInt;
30 import androidx.annotation.RestrictTo;
31 import androidx.annotation.StyleableRes;
32 
33 import org.jspecify.annotations.NonNull;
34 import org.jspecify.annotations.Nullable;
35 import org.xmlpull.v1.XmlPullParser;
36 
37 /**
38  * Compat methods for accessing TypedArray values.
39  *
40  * All the getNamed*() functions added the attribute name match, to take care of potential ID
41  * collision between the private attributes in older OS version (OEM) and the attributes existed in
42  * the newer OS version.
43  * For example, if an private attribute named "abcdefg" in Kitkat has the
44  * same id value as "android:pathData" in Lollipop, we need to match the attribute's namefirst.
45  *
46  */
47 @RestrictTo(LIBRARY_GROUP_PREFIX)
48 public class TypedArrayUtils {
49 
50     private static final String NAMESPACE = "http://schemas.android.com/apk/res/android";
51 
52     /**
53      * @return Whether the current node ofthe  {@link XmlPullParser} has an attribute with the
54      * specified {@code attrName}.
55      */
hasAttribute(@onNull XmlPullParser parser, @NonNull String attrName)56     public static boolean hasAttribute(@NonNull XmlPullParser parser, @NonNull String attrName) {
57         return parser.getAttributeValue(NAMESPACE, attrName) != null;
58     }
59 
60     /**
61      * Retrieves a float attribute value. In addition to the styleable resource ID, we also make
62      * sure that the attribute name matches.
63      *
64      * @return a float value in the {@link TypedArray} with the specified {@code resId}, or
65      * {@code defaultValue} if it does not exist.
66      */
getNamedFloat(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId, float defaultValue)67     public static float getNamedFloat(@NonNull TypedArray a, @NonNull XmlPullParser parser,
68             @NonNull String attrName, @StyleableRes int resId, float defaultValue) {
69         final boolean hasAttr = hasAttribute(parser, attrName);
70         if (!hasAttr) {
71             return defaultValue;
72         } else {
73             return a.getFloat(resId, defaultValue);
74         }
75     }
76 
77     /**
78      * Retrieves a boolean attribute value. In addition to the styleable resource ID, we also make
79      * sure that the attribute name matches.
80      *
81      * @return a boolean value in the {@link TypedArray} with the specified {@code resId}, or
82      * {@code defaultValue} if it does not exist.
83      */
getNamedBoolean(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId, boolean defaultValue)84     public static boolean getNamedBoolean(@NonNull TypedArray a, @NonNull XmlPullParser parser,
85             @NonNull String attrName, @StyleableRes int resId, boolean defaultValue) {
86         final boolean hasAttr = hasAttribute(parser, attrName);
87         if (!hasAttr) {
88             return defaultValue;
89         } else {
90             return a.getBoolean(resId, defaultValue);
91         }
92     }
93 
94     /**
95      * Retrieves an int attribute value. In addition to the styleable resource ID, we also make
96      * sure that the attribute name matches.
97      *
98      * @return an int value in the {@link TypedArray} with the specified {@code resId}, or
99      * {@code defaultValue} if it does not exist.
100      */
getNamedInt(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId, int defaultValue)101     public static int getNamedInt(@NonNull TypedArray a, @NonNull XmlPullParser parser,
102             @NonNull String attrName, @StyleableRes int resId, int defaultValue) {
103         final boolean hasAttr = hasAttribute(parser, attrName);
104         if (!hasAttr) {
105             return defaultValue;
106         } else {
107             return a.getInt(resId, defaultValue);
108         }
109     }
110 
111     /**
112      * Retrieves a color attribute value. In addition to the styleable resource ID, we also make
113      * sure that the attribute name matches.
114      *
115      * @return a color value in the {@link TypedArray} with the specified {@code resId}, or
116      * {@code defaultValue} if it does not exist.
117      */
118     @ColorInt
getNamedColor(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId, @ColorInt int defaultValue)119     public static int getNamedColor(@NonNull TypedArray a, @NonNull XmlPullParser parser,
120             @NonNull String attrName, @StyleableRes int resId, @ColorInt int defaultValue) {
121         final boolean hasAttr = hasAttribute(parser, attrName);
122         if (!hasAttr) {
123             return defaultValue;
124         } else {
125             return a.getColor(resId, defaultValue);
126         }
127     }
128 
129     /**
130      * Retrieves a complex color attribute value. In addition to the styleable resource ID, we also
131      * make sure that the attribute name matches.
132      *
133      * @return a complex color value form the {@link TypedArray} with the specified {@code resId},
134      * or containing {@code defaultValue} if it does not exist.
135      */
getNamedComplexColor(@onNull TypedArray a, @NonNull XmlPullParser parser, Resources.@Nullable Theme theme, @NonNull String attrName, @StyleableRes int resId, @ColorInt int defaultValue)136     public static ComplexColorCompat getNamedComplexColor(@NonNull TypedArray a,
137             @NonNull XmlPullParser parser, Resources.@Nullable Theme theme,
138             @NonNull String attrName, @StyleableRes int resId, @ColorInt int defaultValue) {
139         if (hasAttribute(parser, attrName)) {
140             // first check if is a simple color
141             final TypedValue value = new TypedValue();
142             a.getValue(resId, value);
143             if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
144                     && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
145                 return ComplexColorCompat.from(value.data);
146             }
147 
148             // not a simple color, attempt to inflate complex types
149             final ComplexColorCompat complexColor = ComplexColorCompat.inflate(a.getResources(),
150                     a.getResourceId(resId, 0), theme);
151             if (complexColor != null) return complexColor;
152         }
153         return ComplexColorCompat.from(defaultValue);
154     }
155 
156     /**
157      * Retrieves a color state list object. In addition to the styleable resource ID, we also
158      * make sure that the attribute name matches.
159      *
160      * @return a color state list object form the {@link TypedArray} with the specified
161      * {@code resId}, or null if it does not exist.
162      */
getNamedColorStateList(@onNull TypedArray a, @NonNull XmlPullParser parser, Resources.@Nullable Theme theme, @NonNull String attrName, @StyleableRes int resId)163     public static @Nullable ColorStateList getNamedColorStateList(@NonNull TypedArray a,
164             @NonNull XmlPullParser parser, Resources.@Nullable Theme theme,
165             @NonNull String attrName, @StyleableRes int resId) {
166         if (hasAttribute(parser, attrName)) {
167             final TypedValue value = new TypedValue();
168             a.getValue(resId, value);
169             if (value.type == TypedValue.TYPE_ATTRIBUTE) {
170                 throw new UnsupportedOperationException(
171                         "Failed to resolve attribute at index " + resId + ": " + value);
172             } else if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
173                     && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
174                 // Handle inline color definitions.
175                 return getNamedColorStateListFromInt(value);
176             }
177             return ColorStateListInflaterCompat.inflate(a.getResources(),
178                     a.getResourceId(resId, 0), theme);
179         }
180         return null;
181     }
182 
getNamedColorStateListFromInt( @onNull TypedValue value)183     private static @NonNull ColorStateList getNamedColorStateListFromInt(
184             @NonNull TypedValue value) {
185         // This is copied from ResourcesImpl#getNamedColorStateListFromInt in the platform, but the
186         // ComplexColor caching mechanism has been removed. The practical implication of this is
187         // minimal, since platform caching is only used by Zygote-preloaded resources.
188         return ColorStateList.valueOf(value.data);
189     }
190 
191     /**
192      * Retrieves a resource ID attribute value. In addition to the styleable resource ID, we also
193      * make sure that the attribute name matches.
194      *
195      * @return a resource ID value in the {@link TypedArray} with the specified {@code resId}, or
196      * {@code defaultValue} if it does not exist.
197      */
198     @AnyRes
getNamedResourceId(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId, @AnyRes int defaultValue)199     public static int getNamedResourceId(@NonNull TypedArray a, @NonNull XmlPullParser parser,
200             @NonNull String attrName, @StyleableRes int resId, @AnyRes int defaultValue) {
201         final boolean hasAttr = hasAttribute(parser, attrName);
202         if (!hasAttr) {
203             return defaultValue;
204         } else {
205             return a.getResourceId(resId, defaultValue);
206         }
207     }
208 
209     /**
210      * Retrieves a string attribute value. In addition to the styleable resource ID, we also
211      * make sure that the attribute name matches.
212      *
213      * @return a string value in the {@link TypedArray} with the specified {@code resId}, or
214      * null if it does not exist.
215      */
getNamedString(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId)216     public static @Nullable String getNamedString(@NonNull TypedArray a,
217             @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId) {
218         final boolean hasAttr = hasAttribute(parser, attrName);
219         if (!hasAttr) {
220             return null;
221         } else {
222             return a.getString(resId);
223         }
224     }
225 
226     /**
227      * Retrieve the raw TypedValue for the attribute at <var>index</var>
228      * and return a temporary object holding its data.  This object is only
229      * valid until the next call on to {@link TypedArray}.
230      */
peekNamedValue(@onNull TypedArray a, @NonNull XmlPullParser parser, @NonNull String attrName, int resId)231     public static @Nullable TypedValue peekNamedValue(@NonNull TypedArray a,
232             @NonNull XmlPullParser parser, @NonNull String attrName, int resId) {
233         final boolean hasAttr = hasAttribute(parser, attrName);
234         if (!hasAttr) {
235             return null;
236         } else {
237             return a.peekValue(resId);
238         }
239     }
240 
241     /**
242      * Obtains styled attributes from the theme, if available, or unstyled
243      * resources if the theme is null.
244      */
obtainAttributes(@onNull Resources res, Resources.@Nullable Theme theme, @NonNull AttributeSet set, int @NonNull [] attrs)245     public static @NonNull TypedArray obtainAttributes(@NonNull Resources res,
246             Resources.@Nullable Theme theme, @NonNull AttributeSet set, int @NonNull [] attrs) {
247         if (theme == null) {
248             return res.obtainAttributes(set, attrs);
249         }
250         return theme.obtainStyledAttributes(set, attrs, 0, 0);
251     }
252 
253     /**
254      * @return a boolean value of {@code index}. If it does not exist, a boolean value of
255      * {@code fallbackIndex}. If it still does not exist, {@code defaultValue}.
256      */
getBoolean(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex, boolean defaultValue)257     public static boolean getBoolean(@NonNull TypedArray a, @StyleableRes int index,
258             @StyleableRes int fallbackIndex, boolean defaultValue) {
259         boolean val = a.getBoolean(fallbackIndex, defaultValue);
260         return a.getBoolean(index, val);
261     }
262 
263     /**
264      * @return a drawable value of {@code index}. If it does not exist, a drawable value of
265      * {@code fallbackIndex}. If it still does not exist, {@code null}.
266      */
getDrawable(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex)267     public static @Nullable Drawable getDrawable(@NonNull TypedArray a, @StyleableRes int index,
268             @StyleableRes int fallbackIndex) {
269         Drawable val = a.getDrawable(index);
270         if (val == null) {
271             val = a.getDrawable(fallbackIndex);
272         }
273         return val;
274     }
275 
276     /**
277      * @return an int value of {@code index}. If it does not exist, an int value of
278      * {@code fallbackIndex}. If it still does not exist, {@code defaultValue}.
279      */
getInt(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex, int defaultValue)280     public static int getInt(@NonNull TypedArray a, @StyleableRes int index,
281             @StyleableRes int fallbackIndex, int defaultValue) {
282         int val = a.getInt(fallbackIndex, defaultValue);
283         return a.getInt(index, val);
284     }
285 
286     /**
287      * @return a resource ID value of {@code index}. If it does not exist, a resource ID value of
288      * {@code fallbackIndex}. If it still does not exist, {@code defaultValue}.
289      */
290     @AnyRes
getResourceId(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex, @AnyRes int defaultValue)291     public static int getResourceId(@NonNull TypedArray a, @StyleableRes int index,
292             @StyleableRes int fallbackIndex, @AnyRes int defaultValue) {
293         int val = a.getResourceId(fallbackIndex, defaultValue);
294         return a.getResourceId(index, val);
295     }
296 
297     /**
298      * @return a string value of {@code index}. If it does not exist, a string value of
299      * {@code fallbackIndex}. If it still does not exist, {@code null}.
300      */
getString(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex)301     public static @Nullable String getString(@NonNull TypedArray a, @StyleableRes int index,
302             @StyleableRes int fallbackIndex) {
303         String val = a.getString(index);
304         if (val == null) {
305             val = a.getString(fallbackIndex);
306         }
307         return val;
308     }
309 
310     /**
311      * Retrieves a text attribute value with the specified fallback ID.
312      *
313      * @return a text value of {@code index}. If it does not exist, a text value of
314      * {@code fallbackIndex}. If it still does not exist, {@code null}.
315      */
getText(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex)316     public static @Nullable CharSequence getText(@NonNull TypedArray a, @StyleableRes int index,
317             @StyleableRes int fallbackIndex) {
318         CharSequence val = a.getText(index);
319         if (val == null) {
320             val = a.getText(fallbackIndex);
321         }
322         return val;
323     }
324 
325     /**
326      * Retrieves a string array attribute value with the specified fallback ID.
327      *
328      * @return a string array value of {@code index}. If it does not exist, a string array value
329      * of {@code fallbackIndex}. If it still does not exist, {@code null}.
330      */
getTextArray(@onNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex)331     public static CharSequence @Nullable [] getTextArray(@NonNull TypedArray a,
332             @StyleableRes int index, @StyleableRes int fallbackIndex) {
333         CharSequence[] val = a.getTextArray(index);
334         if (val == null) {
335             val = a.getTextArray(fallbackIndex);
336         }
337         return val;
338     }
339 
340     /**
341      * @return The resource ID value in the {@code context} specified by {@code attr}. If it does
342      * not exist, {@code fallbackAttr}.
343      */
getAttr(@onNull Context context, int attr, int fallbackAttr)344     public static int getAttr(@NonNull Context context, int attr, int fallbackAttr) {
345         TypedValue value = new TypedValue();
346         context.getTheme().resolveAttribute(attr, value, true);
347         if (value.resourceId != 0) {
348             return attr;
349         }
350         return fallbackAttr;
351     }
352 
TypedArrayUtils()353     private TypedArrayUtils() {
354     }
355 }
356