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 libcore.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
45    public CachingIconView(Context context, @Nullable AttributeSet attrs) {
46        super(context, attrs);
47    }
48
49    @Override
50    @RemotableViewMethod(asyncImpl="setImageIconAsync")
51    public void setImageIcon(@Nullable Icon icon) {
52        if (!testAndSetCache(icon)) {
53            mInternalSetDrawable = true;
54            // This calls back to setImageDrawable, make sure we don't clear the cache there.
55            super.setImageIcon(icon);
56            mInternalSetDrawable = false;
57        }
58    }
59
60    @Override
61    public Runnable setImageIconAsync(@Nullable Icon icon) {
62        resetCache();
63        return super.setImageIconAsync(icon);
64    }
65
66    @Override
67    @RemotableViewMethod(asyncImpl="setImageResourceAsync")
68    public void setImageResource(@DrawableRes int resId) {
69        if (!testAndSetCache(resId)) {
70            mInternalSetDrawable = true;
71            // This calls back to setImageDrawable, make sure we don't clear the cache there.
72            super.setImageResource(resId);
73            mInternalSetDrawable = false;
74        }
75    }
76
77    @Override
78    public Runnable setImageResourceAsync(@DrawableRes int resId) {
79        resetCache();
80        return super.setImageResourceAsync(resId);
81    }
82
83    @Override
84    @RemotableViewMethod(asyncImpl="setImageURIAsync")
85    public void setImageURI(@Nullable Uri uri) {
86        resetCache();
87        super.setImageURI(uri);
88    }
89
90    @Override
91    public Runnable setImageURIAsync(@Nullable Uri uri) {
92        resetCache();
93        return super.setImageURIAsync(uri);
94    }
95
96    @Override
97    public void setImageDrawable(@Nullable Drawable drawable) {
98        if (!mInternalSetDrawable) {
99            // Only clear the cache if we were externally called.
100            resetCache();
101        }
102        super.setImageDrawable(drawable);
103    }
104
105    @Override
106    @RemotableViewMethod
107    public void setImageBitmap(Bitmap bm) {
108        resetCache();
109        super.setImageBitmap(bm);
110    }
111
112    @Override
113    protected void onConfigurationChanged(Configuration newConfig) {
114        super.onConfigurationChanged(newConfig);
115        resetCache();
116    }
117
118    /**
119     * @return true if the currently set image is the same as {@param icon}
120     */
121    private synchronized boolean testAndSetCache(Icon icon) {
122        if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
123            String iconPackage = normalizeIconPackage(icon);
124
125            boolean isCached = mLastResId != 0
126                    && icon.getResId() == mLastResId
127                    && Objects.equal(iconPackage, mLastPackage);
128
129            mLastPackage = iconPackage;
130            mLastResId = icon.getResId();
131
132            return isCached;
133        } else {
134            resetCache();
135            return false;
136        }
137    }
138
139    /**
140     * @return true if the currently set image is the same as {@param resId}
141     */
142    private synchronized boolean testAndSetCache(int resId) {
143        boolean isCached;
144        if (resId == 0 || mLastResId == 0) {
145            isCached = false;
146        } else {
147            isCached = resId == mLastResId && null == mLastPackage;
148        }
149        mLastPackage = null;
150        mLastResId = resId;
151        return isCached;
152    }
153
154    /**
155     * Returns the normalized package name of {@param icon}.
156     * @return null if icon is null or if the icons package is null, empty or matches the current
157     *         context. Otherwise returns the icon's package context.
158     */
159    private String normalizeIconPackage(Icon icon) {
160        if (icon == null) {
161            return null;
162        }
163
164        String pkg = icon.getResPackage();
165        if (TextUtils.isEmpty(pkg)) {
166            return null;
167        }
168        if (pkg.equals(mContext.getPackageName())) {
169            return null;
170        }
171        return pkg;
172    }
173
174    private synchronized void resetCache() {
175        mLastResId = 0;
176        mLastPackage = null;
177    }
178}
179