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