1 /*
2  * Copyright (C) 2016 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 com.android.internal.widget;
18 
19 import static com.android.internal.widget.ColoredIconHelper.applyGrayTint;
20 
21 import android.annotation.DrawableRes;
22 import android.annotation.Nullable;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.content.res.TypedArray;
27 import android.graphics.Bitmap;
28 import android.graphics.PorterDuff;
29 import android.graphics.drawable.Drawable;
30 import android.graphics.drawable.Icon;
31 import android.net.Uri;
32 import android.os.Build;
33 import android.text.TextUtils;
34 import android.util.AttributeSet;
35 import android.view.RemotableViewMethod;
36 import android.widget.ImageView;
37 import android.widget.RemoteViews;
38 
39 import com.android.internal.R;
40 
41 import java.util.Objects;
42 import java.util.function.Consumer;
43 
44 /**
45  * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
46  */
47 @RemoteViews.RemoteView
48 public class CachingIconView extends ImageView {
49 
50     private String mLastPackage;
51     private int mLastResId;
52     private boolean mInternalSetDrawable;
53     private boolean mForceHidden;
54     private int mDesiredVisibility;
55     private Consumer<Integer> mOnVisibilityChangedListener;
56     private Consumer<Boolean> mOnForceHiddenChangedListener;
57     private int mIconColor;
58     private int mBackgroundColor;
59     private boolean mWillBeForceHidden;
60 
61     private int mMaxDrawableWidth = -1;
62     private int mMaxDrawableHeight = -1;
63 
CachingIconView(Context context)64     public CachingIconView(Context context) {
65         this(context, null, 0, 0);
66     }
67 
68     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
CachingIconView(Context context, @Nullable AttributeSet attrs)69     public CachingIconView(Context context, @Nullable AttributeSet attrs) {
70         this(context, attrs, 0, 0);
71     }
72 
CachingIconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)73     public CachingIconView(Context context, @Nullable AttributeSet attrs,
74             int defStyleAttr) {
75         this(context, attrs, defStyleAttr, 0);
76     }
77 
CachingIconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)78     public CachingIconView(Context context, @Nullable AttributeSet attrs,
79             int defStyleAttr, int defStyleRes) {
80         super(context, attrs, defStyleAttr, defStyleRes);
81         init(context, attrs, defStyleAttr, defStyleRes);
82     }
83 
init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)84     private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
85             int defStyleRes) {
86         if (attrs == null) {
87             return;
88         }
89 
90         TypedArray ta = context.obtainStyledAttributes(attrs,
91                 R.styleable.CachingIconView, defStyleAttr, defStyleRes);
92         mMaxDrawableWidth = ta.getDimensionPixelSize(R.styleable
93                 .CachingIconView_maxDrawableWidth, -1);
94         mMaxDrawableHeight = ta.getDimensionPixelSize(R.styleable
95                 .CachingIconView_maxDrawableHeight, -1);
96         ta.recycle();
97     }
98 
99     @Override
100     @RemotableViewMethod(asyncImpl="setImageIconAsync")
setImageIcon(@ullable Icon icon)101     public void setImageIcon(@Nullable Icon icon) {
102         if (!testAndSetCache(icon)) {
103             mInternalSetDrawable = true;
104             // This calls back to setImageDrawable, make sure we don't clear the cache there.
105             Drawable drawable = loadSizeRestrictedIcon(icon);
106             if (drawable == null) {
107                 super.setImageIcon(icon);
108             } else {
109                 super.setImageDrawable(drawable);
110             }
111             mInternalSetDrawable = false;
112         }
113     }
114 
115     @Nullable
loadSizeRestrictedIcon(@ullable Icon icon)116     Drawable loadSizeRestrictedIcon(@Nullable Icon icon) {
117         return LocalImageResolver.resolveImage(icon, getContext(), mMaxDrawableWidth,
118                 mMaxDrawableHeight);
119     }
120 
121     @Override
setImageIconAsync(@ullable final Icon icon)122     public Runnable setImageIconAsync(@Nullable final Icon icon) {
123         resetCache();
124         Drawable drawable = loadSizeRestrictedIcon(icon);
125         if (drawable != null) {
126             return () -> setImageDrawable(drawable);
127         }
128         return super.setImageIconAsync(icon);
129     }
130 
131     @Override
132     @RemotableViewMethod(asyncImpl="setImageResourceAsync")
setImageResource(@rawableRes int resId)133     public void setImageResource(@DrawableRes int resId) {
134         if (!testAndSetCache(resId)) {
135             mInternalSetDrawable = true;
136             // This calls back to setImageDrawable, make sure we don't clear the cache there.
137             Drawable drawable = loadSizeRestrictedDrawable(resId);
138             if (drawable == null) {
139                 super.setImageResource(resId);
140             } else {
141                 super.setImageDrawable(drawable);
142             }
143             mInternalSetDrawable = false;
144         }
145     }
146 
147     @Nullable
loadSizeRestrictedDrawable(@rawableRes int resId)148     private Drawable loadSizeRestrictedDrawable(@DrawableRes int resId) {
149         return LocalImageResolver.resolveImage(resId, getContext(), mMaxDrawableWidth,
150                 mMaxDrawableHeight);
151     }
152 
153     @Override
setImageResourceAsync(@rawableRes int resId)154     public Runnable setImageResourceAsync(@DrawableRes int resId) {
155         resetCache();
156         Drawable drawable = loadSizeRestrictedDrawable(resId);
157         if (drawable != null) {
158             return () -> setImageDrawable(drawable);
159         }
160 
161         return super.setImageResourceAsync(resId);
162     }
163 
164     @Override
165     @RemotableViewMethod(asyncImpl="setImageURIAsync")
setImageURI(@ullable Uri uri)166     public void setImageURI(@Nullable Uri uri) {
167         resetCache();
168         Drawable drawable = loadSizeRestrictedUri(uri);
169         if (drawable == null) {
170             super.setImageURI(uri);
171         } else {
172             mInternalSetDrawable = true;
173             super.setImageDrawable(drawable);
174             mInternalSetDrawable = false;
175         }
176     }
177 
178     @Nullable
loadSizeRestrictedUri(@ullable Uri uri)179     private Drawable loadSizeRestrictedUri(@Nullable Uri uri) {
180         return LocalImageResolver.resolveImage(uri, getContext(), mMaxDrawableWidth,
181                 mMaxDrawableHeight);
182     }
183 
184     @Override
setImageURIAsync(@ullable Uri uri)185     public Runnable setImageURIAsync(@Nullable Uri uri) {
186         resetCache();
187         Drawable drawable = loadSizeRestrictedUri(uri);
188         if (drawable == null) {
189             return super.setImageURIAsync(uri);
190         } else {
191             return () -> setImageDrawable(drawable);
192         }
193     }
194 
195     @Override
setImageDrawable(@ullable Drawable drawable)196     public void setImageDrawable(@Nullable Drawable drawable) {
197         if (!mInternalSetDrawable) {
198             // Only clear the cache if we were externally called.
199             resetCache();
200         }
201         super.setImageDrawable(drawable);
202     }
203 
204     @Override
205     @RemotableViewMethod
setImageBitmap(Bitmap bm)206     public void setImageBitmap(Bitmap bm) {
207         resetCache();
208         super.setImageBitmap(bm);
209     }
210 
211     @Override
onConfigurationChanged(Configuration newConfig)212     protected void onConfigurationChanged(Configuration newConfig) {
213         super.onConfigurationChanged(newConfig);
214         resetCache();
215     }
216 
217     /**
218      * @return true if the currently set image is the same as {@param icon}
219      */
testAndSetCache(Icon icon)220     private synchronized boolean testAndSetCache(Icon icon) {
221         if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
222             String iconPackage = normalizeIconPackage(icon);
223 
224             boolean isCached = mLastResId != 0
225                     && icon.getResId() == mLastResId
226                     && Objects.equals(iconPackage, mLastPackage);
227 
228             mLastPackage = iconPackage;
229             mLastResId = icon.getResId();
230 
231             return isCached;
232         } else {
233             resetCache();
234             return false;
235         }
236     }
237 
238     /**
239      * @return true if the currently set image is the same as {@param resId}
240      */
testAndSetCache(int resId)241     private synchronized boolean testAndSetCache(int resId) {
242         boolean isCached;
243         if (resId == 0 || mLastResId == 0) {
244             isCached = false;
245         } else {
246             isCached = resId == mLastResId && null == mLastPackage;
247         }
248         mLastPackage = null;
249         mLastResId = resId;
250         return isCached;
251     }
252 
253     /**
254      * Returns the normalized package name of {@param icon}.
255      * @return null if icon is null or if the icons package is null, empty or matches the current
256      *         context. Otherwise returns the icon's package context.
257      */
normalizeIconPackage(Icon icon)258     private String normalizeIconPackage(Icon icon) {
259         if (icon == null) {
260             return null;
261         }
262 
263         String pkg = icon.getResPackage();
264         if (TextUtils.isEmpty(pkg)) {
265             return null;
266         }
267         if (pkg.equals(mContext.getPackageName())) {
268             return null;
269         }
270         return pkg;
271     }
272 
resetCache()273     private synchronized void resetCache() {
274         mLastResId = 0;
275         mLastPackage = null;
276     }
277 
278     /**
279      * Set the icon to be forcibly hidden, even when it's visibility is changed to visible.
280      * This is necessary since we still want to keep certain views hidden when their visibility
281      * is modified from other sources like the shelf.
282      */
setForceHidden(boolean forceHidden)283     public void setForceHidden(boolean forceHidden) {
284         if (forceHidden != mForceHidden) {
285             mForceHidden = forceHidden;
286             mWillBeForceHidden = false;
287             updateVisibility();
288             if (mOnForceHiddenChangedListener != null) {
289                 mOnForceHiddenChangedListener.accept(forceHidden);
290             }
291         }
292     }
293 
294     @Override
295     @RemotableViewMethod
setVisibility(int visibility)296     public void setVisibility(int visibility) {
297         mDesiredVisibility = visibility;
298         updateVisibility();
299     }
300 
updateVisibility()301     private void updateVisibility() {
302         int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE
303                 : mDesiredVisibility;
304         if (mOnVisibilityChangedListener != null) {
305             mOnVisibilityChangedListener.accept(visibility);
306         }
307         super.setVisibility(visibility);
308     }
309 
setOnVisibilityChangedListener(Consumer<Integer> listener)310     public void setOnVisibilityChangedListener(Consumer<Integer> listener) {
311         mOnVisibilityChangedListener = listener;
312     }
313 
setOnForceHiddenChangedListener(Consumer<Boolean> listener)314     public void setOnForceHiddenChangedListener(Consumer<Boolean> listener) {
315         mOnForceHiddenChangedListener = listener;
316     }
317 
318 
isForceHidden()319     public boolean isForceHidden() {
320         return mForceHidden;
321     }
322 
323     /**
324      * Provides the notification's background color to the icon.  This is only used when the icon
325      * is "inverted".  This should be called before calling {@link #setOriginalIconColor(int)}.
326      */
327     @RemotableViewMethod
setBackgroundColor(int color)328     public void setBackgroundColor(int color) {
329         mBackgroundColor = color;
330     }
331 
332     /**
333      * Sets the icon color. If COLOR_INVALID is set, the icon's color filter will
334      * not be altered. If there is a background drawable, this method uses the value from
335      * {@link #setBackgroundColor(int)} which must have been already called.
336      */
337     @RemotableViewMethod
setOriginalIconColor(int color)338     public void setOriginalIconColor(int color) {
339         mIconColor = color;
340         Drawable background = getBackground();
341         Drawable icon = getDrawable();
342         boolean hasColor = color != ColoredIconHelper.COLOR_INVALID;
343         if (background == null) {
344             // This is the pre-S style -- colored icon with no background.
345             if (hasColor && icon != null) {
346                 icon.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
347             }
348         } else {
349             // When there is a background drawable, color it with the foreground color and
350             // colorize the icon itself with the background color, creating an inverted effect.
351             if (hasColor) {
352                 background.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
353                 if (icon != null) {
354                     icon.mutate().setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP);
355                 }
356             } else {
357                 background.mutate().setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP);
358             }
359         }
360     }
361 
362     /**
363      * Set the icon's color filter: to gray if true, otherwise colored.
364      * If this icon has no original color, this has no effect.
365      */
setGrayedOut(boolean grayedOut)366     public void setGrayedOut(boolean grayedOut) {
367         // If there is a background drawable, then it has the foreground color and the image
368         // drawable has the background color, creating an inverted efffect.
369         Drawable drawable = getBackground();
370         if (drawable == null) {
371             drawable = getDrawable();
372         }
373         applyGrayTint(mContext, drawable, grayedOut, mIconColor);
374     }
375 
getOriginalIconColor()376     public int getOriginalIconColor() {
377         return mIconColor;
378     }
379 
380     /**
381      * @return if the view will be forceHidden after an animation
382      */
willBeForceHidden()383     public boolean willBeForceHidden() {
384         return mWillBeForceHidden;
385     }
386 
387     /**
388      * Set that this view will be force hidden after an animation
389      *
390      * @param forceHidden if it will be forcehidden
391      */
setWillBeForceHidden(boolean forceHidden)392     public void setWillBeForceHidden(boolean forceHidden) {
393         mWillBeForceHidden = forceHidden;
394     }
395 
396     /**
397      * Returns the set maximum width of drawable in pixels. -1 if not set.
398      */
getMaxDrawableWidth()399     public int getMaxDrawableWidth() {
400         return mMaxDrawableWidth;
401     }
402 
403     /**
404      * Returns the set maximum height of drawable in pixels. -1 if not set.
405      */
getMaxDrawableHeight()406     public int getMaxDrawableHeight() {
407         return mMaxDrawableHeight;
408     }
409 }
410