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