1 /*
2  * Copyright (C) 2014 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 
17 package androidx.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 import static androidx.appcompat.content.res.AppCompatResources.getColorStateList;
21 import static androidx.appcompat.widget.ThemeUtils.getDisabledThemeAttrColor;
22 import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColor;
23 import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColorStateList;
24 import static androidx.core.graphics.ColorUtils.compositeColors;
25 
26 import android.annotation.SuppressLint;
27 import android.content.Context;
28 import android.content.res.ColorStateList;
29 import android.content.res.Resources;
30 import android.graphics.Color;
31 import android.graphics.PorterDuff;
32 import android.graphics.PorterDuffColorFilter;
33 import android.graphics.drawable.Drawable;
34 import android.graphics.drawable.Drawable.ConstantState;
35 import android.graphics.drawable.LayerDrawable;
36 import android.os.Build;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.TypedValue;
40 import android.util.Xml;
41 
42 import androidx.annotation.ColorInt;
43 import androidx.annotation.DrawableRes;
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import androidx.annotation.RestrictTo;
47 import androidx.appcompat.R;
48 import androidx.collection.ArrayMap;
49 import androidx.collection.LongSparseArray;
50 import androidx.collection.LruCache;
51 import androidx.collection.SparseArrayCompat;
52 import androidx.core.content.ContextCompat;
53 import androidx.core.graphics.drawable.DrawableCompat;
54 import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
55 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
56 
57 import org.xmlpull.v1.XmlPullParser;
58 import org.xmlpull.v1.XmlPullParserException;
59 
60 import java.lang.ref.WeakReference;
61 import java.util.WeakHashMap;
62 
63 /**
64  * @hide
65  */
66 @RestrictTo(LIBRARY_GROUP)
67 public final class AppCompatDrawableManager {
68 
69     private interface InflateDelegate {
createFromXmlInner(@onNull Context context, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)70         Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
71                 @NonNull AttributeSet attrs, @Nullable Resources.Theme theme);
72     }
73 
74     private static final String TAG = "AppCompatDrawableManag";
75     private static final boolean DEBUG = false;
76     private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
77     private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip";
78 
79     private static final String PLATFORM_VD_CLAZZ = "android.graphics.drawable.VectorDrawable";
80 
81     private static AppCompatDrawableManager INSTANCE;
82 
get()83     public static AppCompatDrawableManager get() {
84         if (INSTANCE == null) {
85             INSTANCE = new AppCompatDrawableManager();
86             installDefaultInflateDelegates(INSTANCE);
87         }
88         return INSTANCE;
89     }
90 
installDefaultInflateDelegates(@onNull AppCompatDrawableManager manager)91     private static void installDefaultInflateDelegates(@NonNull AppCompatDrawableManager manager) {
92         // This sdk version check will affect src:appCompat code path.
93         // Although VectorDrawable exists in Android framework from Lollipop, AppCompat will use the
94         // VectorDrawableCompat before Nougat to utilize the bug fixes in VectorDrawableCompat.
95         if (Build.VERSION.SDK_INT < 24) {
96             manager.addDelegate("vector", new VdcInflateDelegate());
97             manager.addDelegate("animated-vector", new AvdcInflateDelegate());
98         }
99     }
100 
101     private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
102 
103     /**
104      * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
105      * using the default mode using a raw color filter.
106      */
107     private static final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = {
108             R.drawable.abc_textfield_search_default_mtrl_alpha,
109             R.drawable.abc_textfield_default_mtrl_alpha,
110             R.drawable.abc_ab_share_pack_mtrl_alpha
111     };
112 
113     /**
114      * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, using
115      * {@link DrawableCompat}'s tinting functionality.
116      */
117     private static final int[] TINT_COLOR_CONTROL_NORMAL = {
118             R.drawable.abc_ic_commit_search_api_mtrl_alpha,
119             R.drawable.abc_seekbar_tick_mark_material,
120             R.drawable.abc_ic_menu_share_mtrl_alpha,
121             R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
122             R.drawable.abc_ic_menu_cut_mtrl_alpha,
123             R.drawable.abc_ic_menu_selectall_mtrl_alpha,
124             R.drawable.abc_ic_menu_paste_mtrl_am_alpha
125     };
126 
127     /**
128      * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
129      * using a color filter.
130      */
131     private static final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = {
132             R.drawable.abc_textfield_activated_mtrl_alpha,
133             R.drawable.abc_textfield_search_activated_mtrl_alpha,
134             R.drawable.abc_cab_background_top_mtrl_alpha,
135             R.drawable.abc_text_cursor_material,
136             R.drawable.abc_text_select_handle_left_mtrl_dark,
137             R.drawable.abc_text_select_handle_middle_mtrl_dark,
138             R.drawable.abc_text_select_handle_right_mtrl_dark,
139             R.drawable.abc_text_select_handle_left_mtrl_light,
140             R.drawable.abc_text_select_handle_middle_mtrl_light,
141             R.drawable.abc_text_select_handle_right_mtrl_light
142     };
143 
144     /**
145      * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
146      * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter.
147      */
148     private static final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = {
149             R.drawable.abc_popup_background_mtrl_mult,
150             R.drawable.abc_cab_background_internal_bg,
151             R.drawable.abc_menu_hardkey_panel_mtrl_mult
152     };
153 
154     /**
155      * Drawables which should be tinted using a state list containing values of
156      * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
157      */
158     private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
159             R.drawable.abc_tab_indicator_material,
160             R.drawable.abc_textfield_search_material
161     };
162 
163     /**
164      * Drawables which should be tinted using a state list containing values of
165      * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} for the checked
166      * state.
167      */
168     private static final int[] TINT_CHECKABLE_BUTTON_LIST = {
169             R.drawable.abc_btn_check_material,
170             R.drawable.abc_btn_radio_material
171     };
172 
173     private WeakHashMap<Context, SparseArrayCompat<ColorStateList>> mTintLists;
174     private ArrayMap<String, InflateDelegate> mDelegates;
175     private SparseArrayCompat<String> mKnownDrawableIdTags;
176 
177     private final Object mDrawableCacheLock = new Object();
178     private final WeakHashMap<Context, LongSparseArray<WeakReference<Drawable.ConstantState>>>
179             mDrawableCaches = new WeakHashMap<>(0);
180 
181     private TypedValue mTypedValue;
182 
183     private boolean mHasCheckedVectorDrawableSetup;
184 
getDrawable(@onNull Context context, @DrawableRes int resId)185     public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
186         return getDrawable(context, resId, false);
187     }
188 
getDrawable(@onNull Context context, @DrawableRes int resId, boolean failIfNotKnown)189     Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
190             boolean failIfNotKnown) {
191         checkVectorDrawableSetup(context);
192 
193         Drawable drawable = loadDrawableFromDelegates(context, resId);
194         if (drawable == null) {
195             drawable = createDrawableIfNeeded(context, resId);
196         }
197         if (drawable == null) {
198             drawable = ContextCompat.getDrawable(context, resId);
199         }
200 
201         if (drawable != null) {
202             // Tint it if needed
203             drawable = tintDrawable(context, resId, failIfNotKnown, drawable);
204         }
205         if (drawable != null) {
206             // See if we need to 'fix' the drawable
207             DrawableUtils.fixDrawable(drawable);
208         }
209         return drawable;
210     }
211 
onConfigurationChanged(@onNull Context context)212     public void onConfigurationChanged(@NonNull Context context) {
213         synchronized (mDrawableCacheLock) {
214             LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context);
215             if (cache != null) {
216                 // Crude, but we'll just clear the cache when the configuration changes
217                 cache.clear();
218             }
219         }
220     }
221 
createCacheKey(TypedValue tv)222     private static long createCacheKey(TypedValue tv) {
223         return (((long) tv.assetCookie) << 32) | tv.data;
224     }
225 
createDrawableIfNeeded(@onNull Context context, @DrawableRes final int resId)226     private Drawable createDrawableIfNeeded(@NonNull Context context,
227             @DrawableRes final int resId) {
228         if (mTypedValue == null) {
229             mTypedValue = new TypedValue();
230         }
231         final TypedValue tv = mTypedValue;
232         context.getResources().getValue(resId, tv, true);
233         final long key = createCacheKey(tv);
234 
235         Drawable dr = getCachedDrawable(context, key);
236         if (dr != null) {
237             // If we got a cached drawable, return it
238             return dr;
239         }
240 
241         // Else we need to try and create one...
242         if (resId == R.drawable.abc_cab_background_top_material) {
243             dr = new LayerDrawable(new Drawable[]{
244                     getDrawable(context, R.drawable.abc_cab_background_internal_bg),
245                     getDrawable(context, R.drawable.abc_cab_background_top_mtrl_alpha)
246             });
247         }
248 
249         if (dr != null) {
250             dr.setChangingConfigurations(tv.changingConfigurations);
251             // If we reached here then we created a new drawable, add it to the cache
252             addDrawableToCache(context, key, dr);
253         }
254 
255         return dr;
256     }
257 
tintDrawable(@onNull Context context, @DrawableRes int resId, boolean failIfNotKnown, @NonNull Drawable drawable)258     private Drawable tintDrawable(@NonNull Context context, @DrawableRes int resId,
259             boolean failIfNotKnown, @NonNull Drawable drawable) {
260         final ColorStateList tintList = getTintList(context, resId);
261         if (tintList != null) {
262             // First mutate the Drawable, then wrap it and set the tint list
263             if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
264                 drawable = drawable.mutate();
265             }
266             drawable = DrawableCompat.wrap(drawable);
267             DrawableCompat.setTintList(drawable, tintList);
268 
269             // If there is a blending mode specified for the drawable, use it
270             final PorterDuff.Mode tintMode = getTintMode(resId);
271             if (tintMode != null) {
272                 DrawableCompat.setTintMode(drawable, tintMode);
273             }
274         } else if (resId == R.drawable.abc_seekbar_track_material) {
275             LayerDrawable ld = (LayerDrawable) drawable;
276             setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background),
277                     getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE);
278             setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress),
279                     getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE);
280             setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress),
281                     getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
282         } else if (resId == R.drawable.abc_ratingbar_material
283                 || resId == R.drawable.abc_ratingbar_indicator_material
284                 || resId == R.drawable.abc_ratingbar_small_material) {
285             LayerDrawable ld = (LayerDrawable) drawable;
286             setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background),
287                     getDisabledThemeAttrColor(context, R.attr.colorControlNormal),
288                     DEFAULT_MODE);
289             setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress),
290                     getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
291             setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress),
292                     getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
293         } else {
294             final boolean tinted = tintDrawableUsingColorFilter(context, resId, drawable);
295             if (!tinted && failIfNotKnown) {
296                 // If we didn't tint using a ColorFilter, and we're set to fail if we don't
297                 // know the id, return null
298                 drawable = null;
299             }
300         }
301         return drawable;
302     }
303 
loadDrawableFromDelegates(@onNull Context context, @DrawableRes int resId)304     private Drawable loadDrawableFromDelegates(@NonNull Context context, @DrawableRes int resId) {
305         if (mDelegates != null && !mDelegates.isEmpty()) {
306             if (mKnownDrawableIdTags != null) {
307                 final String cachedTagName = mKnownDrawableIdTags.get(resId);
308                 if (SKIP_DRAWABLE_TAG.equals(cachedTagName)
309                         || (cachedTagName != null && mDelegates.get(cachedTagName) == null)) {
310                     // If we don't have a delegate for the drawable tag, or we've been set to
311                     // skip it, fail fast and return null
312                     if (DEBUG) {
313                         Log.d(TAG, "[loadDrawableFromDelegates] Skipping drawable: "
314                                 + context.getResources().getResourceName(resId));
315                     }
316                     return null;
317                 }
318             } else {
319                 // Create an id cache as we'll need one later
320                 mKnownDrawableIdTags = new SparseArrayCompat<>();
321             }
322 
323             if (mTypedValue == null) {
324                 mTypedValue = new TypedValue();
325             }
326             final TypedValue tv = mTypedValue;
327             final Resources res = context.getResources();
328             res.getValue(resId, tv, true);
329 
330             final long key = createCacheKey(tv);
331 
332             Drawable dr = getCachedDrawable(context, key);
333             if (dr != null) {
334                 if (DEBUG) {
335                     Log.i(TAG, "[loadDrawableFromDelegates] Returning cached drawable: " +
336                             context.getResources().getResourceName(resId));
337                 }
338                 // We have a cached drawable, return it!
339                 return dr;
340             }
341 
342             if (tv.string != null && tv.string.toString().endsWith(".xml")) {
343                 // If the resource is an XML file, let's try and parse it
344                 try {
345                     @SuppressLint("ResourceType") final XmlPullParser parser = res.getXml(resId);
346                     final AttributeSet attrs = Xml.asAttributeSet(parser);
347                     int type;
348                     while ((type = parser.next()) != XmlPullParser.START_TAG &&
349                             type != XmlPullParser.END_DOCUMENT) {
350                         // Empty loop
351                     }
352                     if (type != XmlPullParser.START_TAG) {
353                         throw new XmlPullParserException("No start tag found");
354                     }
355 
356                     final String tagName = parser.getName();
357                     // Add the tag name to the cache
358                     mKnownDrawableIdTags.append(resId, tagName);
359 
360                     // Now try and find a delegate for the tag name and inflate if found
361                     final InflateDelegate delegate = mDelegates.get(tagName);
362                     if (delegate != null) {
363                         dr = delegate.createFromXmlInner(context, parser, attrs,
364                                 context.getTheme());
365                     }
366                     if (dr != null) {
367                         // Add it to the drawable cache
368                         dr.setChangingConfigurations(tv.changingConfigurations);
369                         if (addDrawableToCache(context, key, dr) && DEBUG) {
370                             Log.i(TAG, "[loadDrawableFromDelegates] Saved drawable to cache: " +
371                                     context.getResources().getResourceName(resId));
372                         }
373                     }
374                 } catch (Exception e) {
375                     Log.e(TAG, "Exception while inflating drawable", e);
376                 }
377             }
378             if (dr == null) {
379                 // If we reach here then the delegate inflation of the resource failed. Mark it as
380                 // bad so we skip the id next time
381                 mKnownDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG);
382             }
383             return dr;
384         }
385 
386         return null;
387     }
388 
getCachedDrawable(@onNull final Context context, final long key)389     private Drawable getCachedDrawable(@NonNull final Context context, final long key) {
390         synchronized (mDrawableCacheLock) {
391             final LongSparseArray<WeakReference<ConstantState>> cache
392                     = mDrawableCaches.get(context);
393             if (cache == null) {
394                 return null;
395             }
396 
397             final WeakReference<ConstantState> wr = cache.get(key);
398             if (wr != null) {
399                 // We have the key, and the secret
400                 ConstantState entry = wr.get();
401                 if (entry != null) {
402                     return entry.newDrawable(context.getResources());
403                 } else {
404                     // Our entry has been purged
405                     cache.delete(key);
406                 }
407             }
408         }
409         return null;
410     }
411 
addDrawableToCache(@onNull final Context context, final long key, @NonNull final Drawable drawable)412     private boolean addDrawableToCache(@NonNull final Context context, final long key,
413             @NonNull final Drawable drawable) {
414         final ConstantState cs = drawable.getConstantState();
415         if (cs != null) {
416             synchronized (mDrawableCacheLock) {
417                 LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context);
418                 if (cache == null) {
419                     cache = new LongSparseArray<>();
420                     mDrawableCaches.put(context, cache);
421                 }
422                 cache.put(key, new WeakReference<>(cs));
423             }
424             return true;
425         }
426         return false;
427     }
428 
onDrawableLoadedFromResources(@onNull Context context, @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId)429     Drawable onDrawableLoadedFromResources(@NonNull Context context,
430             @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId) {
431         Drawable drawable = loadDrawableFromDelegates(context, resId);
432         if (drawable == null) {
433             drawable = resources.superGetDrawable(resId);
434         }
435         if (drawable != null) {
436             return tintDrawable(context, resId, false, drawable);
437         }
438         return null;
439     }
440 
tintDrawableUsingColorFilter(@onNull Context context, @DrawableRes final int resId, @NonNull Drawable drawable)441     static boolean tintDrawableUsingColorFilter(@NonNull Context context,
442             @DrawableRes final int resId, @NonNull Drawable drawable) {
443         PorterDuff.Mode tintMode = DEFAULT_MODE;
444         boolean colorAttrSet = false;
445         int colorAttr = 0;
446         int alpha = -1;
447 
448         if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) {
449             colorAttr = R.attr.colorControlNormal;
450             colorAttrSet = true;
451         } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) {
452             colorAttr = R.attr.colorControlActivated;
453             colorAttrSet = true;
454         } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) {
455             colorAttr = android.R.attr.colorBackground;
456             colorAttrSet = true;
457             tintMode = PorterDuff.Mode.MULTIPLY;
458         } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
459             colorAttr = android.R.attr.colorForeground;
460             colorAttrSet = true;
461             alpha = Math.round(0.16f * 255);
462         } else if (resId == R.drawable.abc_dialog_material_background) {
463             colorAttr = android.R.attr.colorBackground;
464             colorAttrSet = true;
465         }
466 
467         if (colorAttrSet) {
468             if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
469                 drawable = drawable.mutate();
470             }
471 
472             final int color = getThemeAttrColor(context, colorAttr);
473             drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode));
474 
475             if (alpha != -1) {
476                 drawable.setAlpha(alpha);
477             }
478 
479             if (DEBUG) {
480                 Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted "
481                         + context.getResources().getResourceName(resId) +
482                         " with color: #" + Integer.toHexString(color));
483             }
484             return true;
485         }
486         return false;
487     }
488 
addDelegate(@onNull String tagName, @NonNull InflateDelegate delegate)489     private void addDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) {
490         if (mDelegates == null) {
491             mDelegates = new ArrayMap<>();
492         }
493         mDelegates.put(tagName, delegate);
494     }
495 
removeDelegate(@onNull String tagName, @NonNull InflateDelegate delegate)496     private void removeDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) {
497         if (mDelegates != null && mDelegates.get(tagName) == delegate) {
498             mDelegates.remove(tagName);
499         }
500     }
501 
arrayContains(int[] array, int value)502     private static boolean arrayContains(int[] array, int value) {
503         for (int id : array) {
504             if (id == value) {
505                 return true;
506             }
507         }
508         return false;
509     }
510 
getTintMode(final int resId)511     static PorterDuff.Mode getTintMode(final int resId) {
512         PorterDuff.Mode mode = null;
513 
514         if (resId == R.drawable.abc_switch_thumb_material) {
515             mode = PorterDuff.Mode.MULTIPLY;
516         }
517 
518         return mode;
519     }
520 
getTintList(@onNull Context context, @DrawableRes int resId)521     ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) {
522         // Try the cache first (if it exists)
523         ColorStateList tint = getTintListFromCache(context, resId);
524 
525         if (tint == null) {
526             // ...if the cache did not contain a color state list, try and create one
527             if (resId == R.drawable.abc_edit_text_material) {
528                 tint = getColorStateList(context, R.color.abc_tint_edittext);
529             } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
530                 tint = getColorStateList(context, R.color.abc_tint_switch_track);
531             } else if (resId == R.drawable.abc_switch_thumb_material) {
532                 tint = createSwitchThumbColorStateList(context);
533             } else if (resId == R.drawable.abc_btn_default_mtrl_shape) {
534                 tint = createDefaultButtonColorStateList(context);
535             } else if (resId == R.drawable.abc_btn_borderless_material) {
536                 tint = createBorderlessButtonColorStateList(context);
537             } else if (resId == R.drawable.abc_btn_colored_material) {
538                 tint = createColoredButtonColorStateList(context);
539             } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha
540                     || resId == R.drawable.abc_spinner_textfield_background_material) {
541                 tint = getColorStateList(context, R.color.abc_tint_spinner);
542             } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
543                 tint = getThemeAttrColorStateList(context, R.attr.colorControlNormal);
544             } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
545                 tint = getColorStateList(context, R.color.abc_tint_default);
546             } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) {
547                 tint = getColorStateList(context, R.color.abc_tint_btn_checkable);
548             } else if (resId == R.drawable.abc_seekbar_thumb_material) {
549                 tint = getColorStateList(context, R.color.abc_tint_seek_thumb);
550             }
551 
552             if (tint != null) {
553                 addTintListToCache(context, resId, tint);
554             }
555         }
556         return tint;
557     }
558 
getTintListFromCache(@onNull Context context, @DrawableRes int resId)559     private ColorStateList getTintListFromCache(@NonNull Context context, @DrawableRes int resId) {
560         if (mTintLists != null) {
561             final SparseArrayCompat<ColorStateList> tints = mTintLists.get(context);
562             return tints != null ? tints.get(resId) : null;
563         }
564         return null;
565     }
566 
addTintListToCache(@onNull Context context, @DrawableRes int resId, @NonNull ColorStateList tintList)567     private void addTintListToCache(@NonNull Context context, @DrawableRes int resId,
568             @NonNull ColorStateList tintList) {
569         if (mTintLists == null) {
570             mTintLists = new WeakHashMap<>();
571         }
572         SparseArrayCompat<ColorStateList> themeTints = mTintLists.get(context);
573         if (themeTints == null) {
574             themeTints = new SparseArrayCompat<>();
575             mTintLists.put(context, themeTints);
576         }
577         themeTints.append(resId, tintList);
578     }
579 
createDefaultButtonColorStateList(@onNull Context context)580     private ColorStateList createDefaultButtonColorStateList(@NonNull Context context) {
581         return createButtonColorStateList(context,
582                 getThemeAttrColor(context, R.attr.colorButtonNormal));
583     }
584 
createBorderlessButtonColorStateList(@onNull Context context)585     private ColorStateList createBorderlessButtonColorStateList(@NonNull Context context) {
586         // We ignore the custom tint for borderless buttons
587         return createButtonColorStateList(context, Color.TRANSPARENT);
588     }
589 
createColoredButtonColorStateList(@onNull Context context)590     private ColorStateList createColoredButtonColorStateList(@NonNull Context context) {
591         return createButtonColorStateList(context,
592                 getThemeAttrColor(context, R.attr.colorAccent));
593     }
594 
createButtonColorStateList(@onNull final Context context, @ColorInt final int baseColor)595     private ColorStateList createButtonColorStateList(@NonNull final Context context,
596             @ColorInt final int baseColor) {
597         final int[][] states = new int[4][];
598         final int[] colors = new int[4];
599         int i = 0;
600 
601         final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight);
602         final int disabledColor = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal);
603 
604         // Disabled state
605         states[i] = ThemeUtils.DISABLED_STATE_SET;
606         colors[i] = disabledColor;
607         i++;
608 
609         states[i] = ThemeUtils.PRESSED_STATE_SET;
610         colors[i] = compositeColors(colorControlHighlight, baseColor);
611         i++;
612 
613         states[i] = ThemeUtils.FOCUSED_STATE_SET;
614         colors[i] = compositeColors(colorControlHighlight, baseColor);
615         i++;
616 
617         // Default enabled state
618         states[i] = ThemeUtils.EMPTY_STATE_SET;
619         colors[i] = baseColor;
620         i++;
621 
622         return new ColorStateList(states, colors);
623     }
624 
createSwitchThumbColorStateList(Context context)625     private ColorStateList createSwitchThumbColorStateList(Context context) {
626         final int[][] states = new int[3][];
627         final int[] colors = new int[3];
628         int i = 0;
629 
630         final ColorStateList thumbColor = getThemeAttrColorStateList(context,
631                 R.attr.colorSwitchThumbNormal);
632 
633         if (thumbColor != null && thumbColor.isStateful()) {
634             // If colorSwitchThumbNormal is a valid ColorStateList, extract the default and
635             // disabled colors from it
636 
637             // Disabled state
638             states[i] = ThemeUtils.DISABLED_STATE_SET;
639             colors[i] = thumbColor.getColorForState(states[i], 0);
640             i++;
641 
642             states[i] = ThemeUtils.CHECKED_STATE_SET;
643             colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
644             i++;
645 
646             // Default enabled state
647             states[i] = ThemeUtils.EMPTY_STATE_SET;
648             colors[i] = thumbColor.getDefaultColor();
649             i++;
650         } else {
651             // Else we'll use an approximation using the default disabled alpha
652 
653             // Disabled state
654             states[i] = ThemeUtils.DISABLED_STATE_SET;
655             colors[i] = getDisabledThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
656             i++;
657 
658             states[i] = ThemeUtils.CHECKED_STATE_SET;
659             colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
660             i++;
661 
662             // Default enabled state
663             states[i] = ThemeUtils.EMPTY_STATE_SET;
664             colors[i] = getThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
665             i++;
666         }
667 
668         return new ColorStateList(states, colors);
669     }
670 
671     private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
672 
ColorFilterLruCache(int maxSize)673         public ColorFilterLruCache(int maxSize) {
674             super(maxSize);
675         }
676 
get(int color, PorterDuff.Mode mode)677         PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
678             return get(generateCacheKey(color, mode));
679         }
680 
put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter)681         PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
682             return put(generateCacheKey(color, mode), filter);
683         }
684 
generateCacheKey(int color, PorterDuff.Mode mode)685         private static int generateCacheKey(int color, PorterDuff.Mode mode) {
686             int hashCode = 1;
687             hashCode = 31 * hashCode + color;
688             hashCode = 31 * hashCode + mode.hashCode();
689             return hashCode;
690         }
691     }
692 
tintDrawable(Drawable drawable, TintInfo tint, int[] state)693     static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
694         if (DrawableUtils.canSafelyMutateDrawable(drawable)
695                 && drawable.mutate() != drawable) {
696             Log.d(TAG, "Mutated drawable is not the same instance as the input.");
697             return;
698         }
699 
700         if (tint.mHasTintList || tint.mHasTintMode) {
701             drawable.setColorFilter(createTintFilter(
702                     tint.mHasTintList ? tint.mTintList : null,
703                     tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
704                     state));
705         } else {
706             drawable.clearColorFilter();
707         }
708 
709         if (Build.VERSION.SDK_INT <= 23) {
710             // Pre-v23 there is no guarantee that a state change will invoke an invalidation,
711             // so we force it ourselves
712             drawable.invalidateSelf();
713         }
714     }
715 
createTintFilter(ColorStateList tint, PorterDuff.Mode tintMode, final int[] state)716     private static PorterDuffColorFilter createTintFilter(ColorStateList tint,
717             PorterDuff.Mode tintMode, final int[] state) {
718         if (tint == null || tintMode == null) {
719             return null;
720         }
721         final int color = tint.getColorForState(state, Color.TRANSPARENT);
722         return getPorterDuffColorFilter(color, tintMode);
723     }
724 
getPorterDuffColorFilter(int color, PorterDuff.Mode mode)725     public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
726         // First, lets see if the cache already contains the color filter
727         PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
728 
729         if (filter == null) {
730             // Cache miss, so create a color filter and add it to the cache
731             filter = new PorterDuffColorFilter(color, mode);
732             COLOR_FILTER_CACHE.put(color, mode, filter);
733         }
734 
735         return filter;
736     }
737 
setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode)738     private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
739         if (DrawableUtils.canSafelyMutateDrawable(d)) {
740             d = d.mutate();
741         }
742         d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE : mode));
743     }
744 
checkVectorDrawableSetup(@onNull Context context)745     private void checkVectorDrawableSetup(@NonNull Context context) {
746         if (mHasCheckedVectorDrawableSetup) {
747             // We've already checked so return now...
748             return;
749         }
750         // Here we will check that a known Vector drawable resource inside AppCompat can be
751         // correctly decoded
752         mHasCheckedVectorDrawableSetup = true;
753         final Drawable d = getDrawable(context, R.drawable.abc_vector_test);
754         if (d == null || !isVectorDrawable(d)) {
755             mHasCheckedVectorDrawableSetup = false;
756             throw new IllegalStateException("This app has been built with an incorrect "
757                     + "configuration. Please configure your build for VectorDrawableCompat.");
758         }
759     }
760 
isVectorDrawable(@onNull Drawable d)761     private static boolean isVectorDrawable(@NonNull Drawable d) {
762         return d instanceof VectorDrawableCompat
763                 || PLATFORM_VD_CLAZZ.equals(d.getClass().getName());
764     }
765 
766     private static class VdcInflateDelegate implements InflateDelegate {
VdcInflateDelegate()767         VdcInflateDelegate() {
768         }
769 
770         @Override
createFromXmlInner(@onNull Context context, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)771         public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
772                 @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) {
773             try {
774                 return VectorDrawableCompat
775                         .createFromXmlInner(context.getResources(), parser, attrs, theme);
776             } catch (Exception e) {
777                 Log.e("VdcInflateDelegate", "Exception while inflating <vector>", e);
778                 return null;
779             }
780         }
781     }
782 
783     private static class AvdcInflateDelegate implements InflateDelegate {
AvdcInflateDelegate()784         AvdcInflateDelegate() {
785         }
786 
787         @Override
createFromXmlInner(@onNull Context context, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)788         public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
789                 @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) {
790             try {
791                 return AnimatedVectorDrawableCompat
792                         .createFromXmlInner(context, context.getResources(), parser, attrs, theme);
793             } catch (Exception e) {
794                 Log.e("AvdcInflateDelegate", "Exception while inflating <animated-vector>", e);
795                 return null;
796             }
797         }
798     }
799 }
800