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 android.annotation.DrawableRes;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Bitmap;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.Icon;
27 import android.net.Uri;
28 import android.text.TextUtils;
29 import android.util.AttributeSet;
30 import android.view.RemotableViewMethod;
31 import android.widget.ImageView;
32 import android.widget.RemoteViews;
33 
34 import java.util.Objects;
35 import java.util.function.Consumer;
36 
37 /**
38  * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
39  */
40 @RemoteViews.RemoteView
41 public class CachingIconView extends ImageView {
42 
43     private String mLastPackage;
44     private int mLastResId;
45     private boolean mInternalSetDrawable;
46     private boolean mForceHidden;
47     private int mDesiredVisibility;
48     private Consumer<Integer> mOnVisibilityChangedListener;
49     private Consumer<Boolean> mOnForceHiddenChangedListener;
50     private int mIconColor;
51     private boolean mWillBeForceHidden;
52 
53     @UnsupportedAppUsage
CachingIconView(Context context, @Nullable AttributeSet attrs)54     public CachingIconView(Context context, @Nullable AttributeSet attrs) {
55         super(context, attrs);
56     }
57 
58     @Override
59     @RemotableViewMethod(asyncImpl="setImageIconAsync")
setImageIcon(@ullable Icon icon)60     public void setImageIcon(@Nullable Icon icon) {
61         if (!testAndSetCache(icon)) {
62             mInternalSetDrawable = true;
63             // This calls back to setImageDrawable, make sure we don't clear the cache there.
64             super.setImageIcon(icon);
65             mInternalSetDrawable = false;
66         }
67     }
68 
69     @Override
setImageIconAsync(@ullable Icon icon)70     public Runnable setImageIconAsync(@Nullable Icon icon) {
71         resetCache();
72         return super.setImageIconAsync(icon);
73     }
74 
75     @Override
76     @RemotableViewMethod(asyncImpl="setImageResourceAsync")
setImageResource(@rawableRes int resId)77     public void setImageResource(@DrawableRes int resId) {
78         if (!testAndSetCache(resId)) {
79             mInternalSetDrawable = true;
80             // This calls back to setImageDrawable, make sure we don't clear the cache there.
81             super.setImageResource(resId);
82             mInternalSetDrawable = false;
83         }
84     }
85 
86     @Override
setImageResourceAsync(@rawableRes int resId)87     public Runnable setImageResourceAsync(@DrawableRes int resId) {
88         resetCache();
89         return super.setImageResourceAsync(resId);
90     }
91 
92     @Override
93     @RemotableViewMethod(asyncImpl="setImageURIAsync")
setImageURI(@ullable Uri uri)94     public void setImageURI(@Nullable Uri uri) {
95         resetCache();
96         super.setImageURI(uri);
97     }
98 
99     @Override
setImageURIAsync(@ullable Uri uri)100     public Runnable setImageURIAsync(@Nullable Uri uri) {
101         resetCache();
102         return super.setImageURIAsync(uri);
103     }
104 
105     @Override
setImageDrawable(@ullable Drawable drawable)106     public void setImageDrawable(@Nullable Drawable drawable) {
107         if (!mInternalSetDrawable) {
108             // Only clear the cache if we were externally called.
109             resetCache();
110         }
111         super.setImageDrawable(drawable);
112     }
113 
114     @Override
115     @RemotableViewMethod
setImageBitmap(Bitmap bm)116     public void setImageBitmap(Bitmap bm) {
117         resetCache();
118         super.setImageBitmap(bm);
119     }
120 
121     @Override
onConfigurationChanged(Configuration newConfig)122     protected void onConfigurationChanged(Configuration newConfig) {
123         super.onConfigurationChanged(newConfig);
124         resetCache();
125     }
126 
127     /**
128      * @return true if the currently set image is the same as {@param icon}
129      */
testAndSetCache(Icon icon)130     private synchronized boolean testAndSetCache(Icon icon) {
131         if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
132             String iconPackage = normalizeIconPackage(icon);
133 
134             boolean isCached = mLastResId != 0
135                     && icon.getResId() == mLastResId
136                     && Objects.equals(iconPackage, mLastPackage);
137 
138             mLastPackage = iconPackage;
139             mLastResId = icon.getResId();
140 
141             return isCached;
142         } else {
143             resetCache();
144             return false;
145         }
146     }
147 
148     /**
149      * @return true if the currently set image is the same as {@param resId}
150      */
testAndSetCache(int resId)151     private synchronized boolean testAndSetCache(int resId) {
152         boolean isCached;
153         if (resId == 0 || mLastResId == 0) {
154             isCached = false;
155         } else {
156             isCached = resId == mLastResId && null == mLastPackage;
157         }
158         mLastPackage = null;
159         mLastResId = resId;
160         return isCached;
161     }
162 
163     /**
164      * Returns the normalized package name of {@param icon}.
165      * @return null if icon is null or if the icons package is null, empty or matches the current
166      *         context. Otherwise returns the icon's package context.
167      */
normalizeIconPackage(Icon icon)168     private String normalizeIconPackage(Icon icon) {
169         if (icon == null) {
170             return null;
171         }
172 
173         String pkg = icon.getResPackage();
174         if (TextUtils.isEmpty(pkg)) {
175             return null;
176         }
177         if (pkg.equals(mContext.getPackageName())) {
178             return null;
179         }
180         return pkg;
181     }
182 
resetCache()183     private synchronized void resetCache() {
184         mLastResId = 0;
185         mLastPackage = null;
186     }
187 
188     /**
189      * Set the icon to be forcibly hidden, even when it's visibility is changed to visible.
190      * This is necessary since we still want to keep certain views hidden when their visibility
191      * is modified from other sources like the shelf.
192      */
setForceHidden(boolean forceHidden)193     public void setForceHidden(boolean forceHidden) {
194         if (forceHidden != mForceHidden) {
195             mForceHidden = forceHidden;
196             mWillBeForceHidden = false;
197             updateVisibility();
198             if (mOnForceHiddenChangedListener != null) {
199                 mOnForceHiddenChangedListener.accept(forceHidden);
200             }
201         }
202     }
203 
204     @Override
205     @RemotableViewMethod
setVisibility(int visibility)206     public void setVisibility(int visibility) {
207         mDesiredVisibility = visibility;
208         updateVisibility();
209     }
210 
updateVisibility()211     private void updateVisibility() {
212         int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE
213                 : mDesiredVisibility;
214         if (mOnVisibilityChangedListener != null) {
215             mOnVisibilityChangedListener.accept(visibility);
216         }
217         super.setVisibility(visibility);
218     }
219 
setOnVisibilityChangedListener(Consumer<Integer> listener)220     public void setOnVisibilityChangedListener(Consumer<Integer> listener) {
221         mOnVisibilityChangedListener = listener;
222     }
223 
setOnForceHiddenChangedListener(Consumer<Boolean> listener)224     public void setOnForceHiddenChangedListener(Consumer<Boolean> listener) {
225         mOnForceHiddenChangedListener = listener;
226     }
227 
228 
isForceHidden()229     public boolean isForceHidden() {
230         return mForceHidden;
231     }
232 
233     @RemotableViewMethod
setOriginalIconColor(int color)234     public void setOriginalIconColor(int color) {
235         mIconColor = color;
236     }
237 
getOriginalIconColor()238     public int getOriginalIconColor() {
239         return mIconColor;
240     }
241 
242     /**
243      * @return if the view will be forceHidden after an animation
244      */
willBeForceHidden()245     public boolean willBeForceHidden() {
246         return mWillBeForceHidden;
247     }
248 
249     /**
250      * Set that this view will be force hidden after an animation
251      *
252      * @param forceHidden if it will be forcehidden
253      */
setWillBeForceHidden(boolean forceHidden)254     public void setWillBeForceHidden(boolean forceHidden) {
255         mWillBeForceHidden = forceHidden;
256     }
257 }
258