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