• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics.Bitmap;
27 import android.graphics.PorterDuff;
28 import android.graphics.drawable.Drawable;
29 import android.graphics.drawable.Icon;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.text.TextUtils;
33 import android.util.AttributeSet;
34 import android.view.RemotableViewMethod;
35 import android.widget.ImageView;
36 import android.widget.RemoteViews;
37 
38 import java.util.Objects;
39 import java.util.function.Consumer;
40 
41 /**
42  * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
43  */
44 @RemoteViews.RemoteView
45 public class CachingIconView extends ImageView {
46 
47     private String mLastPackage;
48     private int mLastResId;
49     private boolean mInternalSetDrawable;
50     private boolean mForceHidden;
51     private int mDesiredVisibility;
52     private Consumer<Integer> mOnVisibilityChangedListener;
53     private Consumer<Boolean> mOnForceHiddenChangedListener;
54     private int mIconColor;
55     private int mBackgroundColor;
56     private boolean mWillBeForceHidden;
57 
58     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
CachingIconView(Context context, @Nullable AttributeSet attrs)59     public CachingIconView(Context context, @Nullable AttributeSet attrs) {
60         super(context, attrs);
61     }
62 
63     @Override
64     @RemotableViewMethod(asyncImpl="setImageIconAsync")
setImageIcon(@ullable Icon icon)65     public void setImageIcon(@Nullable Icon icon) {
66         if (!testAndSetCache(icon)) {
67             mInternalSetDrawable = true;
68             // This calls back to setImageDrawable, make sure we don't clear the cache there.
69             super.setImageIcon(icon);
70             mInternalSetDrawable = false;
71         }
72     }
73 
74     @Override
setImageIconAsync(@ullable Icon icon)75     public Runnable setImageIconAsync(@Nullable Icon icon) {
76         resetCache();
77         return super.setImageIconAsync(icon);
78     }
79 
80     @Override
81     @RemotableViewMethod(asyncImpl="setImageResourceAsync")
setImageResource(@rawableRes int resId)82     public void setImageResource(@DrawableRes int resId) {
83         if (!testAndSetCache(resId)) {
84             mInternalSetDrawable = true;
85             // This calls back to setImageDrawable, make sure we don't clear the cache there.
86             super.setImageResource(resId);
87             mInternalSetDrawable = false;
88         }
89     }
90 
91     @Override
setImageResourceAsync(@rawableRes int resId)92     public Runnable setImageResourceAsync(@DrawableRes int resId) {
93         resetCache();
94         return super.setImageResourceAsync(resId);
95     }
96 
97     @Override
98     @RemotableViewMethod(asyncImpl="setImageURIAsync")
setImageURI(@ullable Uri uri)99     public void setImageURI(@Nullable Uri uri) {
100         resetCache();
101         super.setImageURI(uri);
102     }
103 
104     @Override
setImageURIAsync(@ullable Uri uri)105     public Runnable setImageURIAsync(@Nullable Uri uri) {
106         resetCache();
107         return super.setImageURIAsync(uri);
108     }
109 
110     @Override
setImageDrawable(@ullable Drawable drawable)111     public void setImageDrawable(@Nullable Drawable drawable) {
112         if (!mInternalSetDrawable) {
113             // Only clear the cache if we were externally called.
114             resetCache();
115         }
116         super.setImageDrawable(drawable);
117     }
118 
119     @Override
120     @RemotableViewMethod
setImageBitmap(Bitmap bm)121     public void setImageBitmap(Bitmap bm) {
122         resetCache();
123         super.setImageBitmap(bm);
124     }
125 
126     @Override
onConfigurationChanged(Configuration newConfig)127     protected void onConfigurationChanged(Configuration newConfig) {
128         super.onConfigurationChanged(newConfig);
129         resetCache();
130     }
131 
132     /**
133      * @return true if the currently set image is the same as {@param icon}
134      */
testAndSetCache(Icon icon)135     private synchronized boolean testAndSetCache(Icon icon) {
136         if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
137             String iconPackage = normalizeIconPackage(icon);
138 
139             boolean isCached = mLastResId != 0
140                     && icon.getResId() == mLastResId
141                     && Objects.equals(iconPackage, mLastPackage);
142 
143             mLastPackage = iconPackage;
144             mLastResId = icon.getResId();
145 
146             return isCached;
147         } else {
148             resetCache();
149             return false;
150         }
151     }
152 
153     /**
154      * @return true if the currently set image is the same as {@param resId}
155      */
testAndSetCache(int resId)156     private synchronized boolean testAndSetCache(int resId) {
157         boolean isCached;
158         if (resId == 0 || mLastResId == 0) {
159             isCached = false;
160         } else {
161             isCached = resId == mLastResId && null == mLastPackage;
162         }
163         mLastPackage = null;
164         mLastResId = resId;
165         return isCached;
166     }
167 
168     /**
169      * Returns the normalized package name of {@param icon}.
170      * @return null if icon is null or if the icons package is null, empty or matches the current
171      *         context. Otherwise returns the icon's package context.
172      */
normalizeIconPackage(Icon icon)173     private String normalizeIconPackage(Icon icon) {
174         if (icon == null) {
175             return null;
176         }
177 
178         String pkg = icon.getResPackage();
179         if (TextUtils.isEmpty(pkg)) {
180             return null;
181         }
182         if (pkg.equals(mContext.getPackageName())) {
183             return null;
184         }
185         return pkg;
186     }
187 
resetCache()188     private synchronized void resetCache() {
189         mLastResId = 0;
190         mLastPackage = null;
191     }
192 
193     /**
194      * Set the icon to be forcibly hidden, even when it's visibility is changed to visible.
195      * This is necessary since we still want to keep certain views hidden when their visibility
196      * is modified from other sources like the shelf.
197      */
setForceHidden(boolean forceHidden)198     public void setForceHidden(boolean forceHidden) {
199         if (forceHidden != mForceHidden) {
200             mForceHidden = forceHidden;
201             mWillBeForceHidden = false;
202             updateVisibility();
203             if (mOnForceHiddenChangedListener != null) {
204                 mOnForceHiddenChangedListener.accept(forceHidden);
205             }
206         }
207     }
208 
209     @Override
210     @RemotableViewMethod
setVisibility(int visibility)211     public void setVisibility(int visibility) {
212         mDesiredVisibility = visibility;
213         updateVisibility();
214     }
215 
updateVisibility()216     private void updateVisibility() {
217         int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE
218                 : mDesiredVisibility;
219         if (mOnVisibilityChangedListener != null) {
220             mOnVisibilityChangedListener.accept(visibility);
221         }
222         super.setVisibility(visibility);
223     }
224 
setOnVisibilityChangedListener(Consumer<Integer> listener)225     public void setOnVisibilityChangedListener(Consumer<Integer> listener) {
226         mOnVisibilityChangedListener = listener;
227     }
228 
setOnForceHiddenChangedListener(Consumer<Boolean> listener)229     public void setOnForceHiddenChangedListener(Consumer<Boolean> listener) {
230         mOnForceHiddenChangedListener = listener;
231     }
232 
233 
isForceHidden()234     public boolean isForceHidden() {
235         return mForceHidden;
236     }
237 
238     /**
239      * Provides the notification's background color to the icon.  This is only used when the icon
240      * is "inverted".  This should be called before calling {@link #setOriginalIconColor(int)}.
241      */
242     @RemotableViewMethod
setBackgroundColor(int color)243     public void setBackgroundColor(int color) {
244         mBackgroundColor = color;
245     }
246 
247     /**
248      * Sets the icon color. If COLOR_INVALID is set, the icon's color filter will
249      * not be altered. If there is a background drawable, this method uses the value from
250      * {@link #setBackgroundColor(int)} which must have been already called.
251      */
252     @RemotableViewMethod
setOriginalIconColor(int color)253     public void setOriginalIconColor(int color) {
254         mIconColor = color;
255         Drawable background = getBackground();
256         Drawable icon = getDrawable();
257         boolean hasColor = color != ColoredIconHelper.COLOR_INVALID;
258         if (background == null) {
259             // This is the pre-S style -- colored icon with no background.
260             if (hasColor && icon != null) {
261                 icon.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
262             }
263         } else {
264             // When there is a background drawable, color it with the foreground color and
265             // colorize the icon itself with the background color, creating an inverted effect.
266             if (hasColor) {
267                 background.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
268                 if (icon != null) {
269                     icon.mutate().setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP);
270                 }
271             } else {
272                 background.mutate().setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP);
273             }
274         }
275     }
276 
277     /**
278      * Set the icon's color filter: to gray if true, otherwise colored.
279      * If this icon has no original color, this has no effect.
280      */
setGrayedOut(boolean grayedOut)281     public void setGrayedOut(boolean grayedOut) {
282         // If there is a background drawable, then it has the foreground color and the image
283         // drawable has the background color, creating an inverted efffect.
284         Drawable drawable = getBackground();
285         if (drawable == null) {
286             drawable = getDrawable();
287         }
288         applyGrayTint(mContext, drawable, grayedOut, mIconColor);
289     }
290 
getOriginalIconColor()291     public int getOriginalIconColor() {
292         return mIconColor;
293     }
294 
295     /**
296      * @return if the view will be forceHidden after an animation
297      */
willBeForceHidden()298     public boolean willBeForceHidden() {
299         return mWillBeForceHidden;
300     }
301 
302     /**
303      * Set that this view will be force hidden after an animation
304      *
305      * @param forceHidden if it will be forcehidden
306      */
setWillBeForceHidden(boolean forceHidden)307     public void setWillBeForceHidden(boolean forceHidden) {
308         mWillBeForceHidden = forceHidden;
309     }
310 }
311