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