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 
36 /**
37  * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
38  */
39 @RemoteViews.RemoteView
40 public class CachingIconView extends ImageView {
41 
42     private String mLastPackage;
43     private int mLastResId;
44     private boolean mInternalSetDrawable;
45     private boolean mForceHidden;
46     private int mDesiredVisibility;
47 
48     @UnsupportedAppUsage
CachingIconView(Context context, @Nullable AttributeSet attrs)49     public CachingIconView(Context context, @Nullable AttributeSet attrs) {
50         super(context, attrs);
51     }
52 
53     @Override
54     @RemotableViewMethod(asyncImpl="setImageIconAsync")
setImageIcon(@ullable Icon icon)55     public void setImageIcon(@Nullable Icon icon) {
56         if (!testAndSetCache(icon)) {
57             mInternalSetDrawable = true;
58             // This calls back to setImageDrawable, make sure we don't clear the cache there.
59             super.setImageIcon(icon);
60             mInternalSetDrawable = false;
61         }
62     }
63 
64     @Override
setImageIconAsync(@ullable Icon icon)65     public Runnable setImageIconAsync(@Nullable Icon icon) {
66         resetCache();
67         return super.setImageIconAsync(icon);
68     }
69 
70     @Override
71     @RemotableViewMethod(asyncImpl="setImageResourceAsync")
setImageResource(@rawableRes int resId)72     public void setImageResource(@DrawableRes int resId) {
73         if (!testAndSetCache(resId)) {
74             mInternalSetDrawable = true;
75             // This calls back to setImageDrawable, make sure we don't clear the cache there.
76             super.setImageResource(resId);
77             mInternalSetDrawable = false;
78         }
79     }
80 
81     @Override
setImageResourceAsync(@rawableRes int resId)82     public Runnable setImageResourceAsync(@DrawableRes int resId) {
83         resetCache();
84         return super.setImageResourceAsync(resId);
85     }
86 
87     @Override
88     @RemotableViewMethod(asyncImpl="setImageURIAsync")
setImageURI(@ullable Uri uri)89     public void setImageURI(@Nullable Uri uri) {
90         resetCache();
91         super.setImageURI(uri);
92     }
93 
94     @Override
setImageURIAsync(@ullable Uri uri)95     public Runnable setImageURIAsync(@Nullable Uri uri) {
96         resetCache();
97         return super.setImageURIAsync(uri);
98     }
99 
100     @Override
setImageDrawable(@ullable Drawable drawable)101     public void setImageDrawable(@Nullable Drawable drawable) {
102         if (!mInternalSetDrawable) {
103             // Only clear the cache if we were externally called.
104             resetCache();
105         }
106         super.setImageDrawable(drawable);
107     }
108 
109     @Override
110     @RemotableViewMethod
setImageBitmap(Bitmap bm)111     public void setImageBitmap(Bitmap bm) {
112         resetCache();
113         super.setImageBitmap(bm);
114     }
115 
116     @Override
onConfigurationChanged(Configuration newConfig)117     protected void onConfigurationChanged(Configuration newConfig) {
118         super.onConfigurationChanged(newConfig);
119         resetCache();
120     }
121 
122     /**
123      * @return true if the currently set image is the same as {@param icon}
124      */
testAndSetCache(Icon icon)125     private synchronized boolean testAndSetCache(Icon icon) {
126         if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
127             String iconPackage = normalizeIconPackage(icon);
128 
129             boolean isCached = mLastResId != 0
130                     && icon.getResId() == mLastResId
131                     && Objects.equals(iconPackage, mLastPackage);
132 
133             mLastPackage = iconPackage;
134             mLastResId = icon.getResId();
135 
136             return isCached;
137         } else {
138             resetCache();
139             return false;
140         }
141     }
142 
143     /**
144      * @return true if the currently set image is the same as {@param resId}
145      */
testAndSetCache(int resId)146     private synchronized boolean testAndSetCache(int resId) {
147         boolean isCached;
148         if (resId == 0 || mLastResId == 0) {
149             isCached = false;
150         } else {
151             isCached = resId == mLastResId && null == mLastPackage;
152         }
153         mLastPackage = null;
154         mLastResId = resId;
155         return isCached;
156     }
157 
158     /**
159      * Returns the normalized package name of {@param icon}.
160      * @return null if icon is null or if the icons package is null, empty or matches the current
161      *         context. Otherwise returns the icon's package context.
162      */
normalizeIconPackage(Icon icon)163     private String normalizeIconPackage(Icon icon) {
164         if (icon == null) {
165             return null;
166         }
167 
168         String pkg = icon.getResPackage();
169         if (TextUtils.isEmpty(pkg)) {
170             return null;
171         }
172         if (pkg.equals(mContext.getPackageName())) {
173             return null;
174         }
175         return pkg;
176     }
177 
resetCache()178     private synchronized void resetCache() {
179         mLastResId = 0;
180         mLastPackage = null;
181     }
182 
183     /**
184      * Set the icon to be forcibly hidden, even when it's visibility is changed to visible.
185      */
setForceHidden(boolean forceHidden)186     public void setForceHidden(boolean forceHidden) {
187         mForceHidden = forceHidden;
188         updateVisibility();
189     }
190 
191     @Override
192     @RemotableViewMethod
setVisibility(int visibility)193     public void setVisibility(int visibility) {
194         mDesiredVisibility = visibility;
195         updateVisibility();
196     }
197 
updateVisibility()198     private void updateVisibility() {
199         int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE
200                 : mDesiredVisibility;
201         super.setVisibility(visibility);
202     }
203 }
204