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.content.res.TypedArray; 27 import android.graphics.Bitmap; 28 import android.graphics.PorterDuff; 29 import android.graphics.drawable.Drawable; 30 import android.graphics.drawable.Icon; 31 import android.net.Uri; 32 import android.os.Build; 33 import android.text.TextUtils; 34 import android.util.AttributeSet; 35 import android.view.RemotableViewMethod; 36 import android.widget.ImageView; 37 import android.widget.RemoteViews; 38 39 import com.android.internal.R; 40 41 import java.util.Objects; 42 import java.util.function.Consumer; 43 44 /** 45 * An ImageView for displaying an Icon. Avoids reloading the Icon when possible. 46 */ 47 @RemoteViews.RemoteView 48 public class CachingIconView extends ImageView { 49 50 private String mLastPackage; 51 private int mLastResId; 52 private boolean mInternalSetDrawable; 53 private boolean mForceHidden; 54 private int mDesiredVisibility; 55 private Consumer<Integer> mOnVisibilityChangedListener; 56 private Consumer<Boolean> mOnForceHiddenChangedListener; 57 private int mIconColor; 58 private int mBackgroundColor; 59 private boolean mWillBeForceHidden; 60 61 private int mMaxDrawableWidth = -1; 62 private int mMaxDrawableHeight = -1; 63 CachingIconView(Context context)64 public CachingIconView(Context context) { 65 this(context, null, 0, 0); 66 } 67 68 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) CachingIconView(Context context, @Nullable AttributeSet attrs)69 public CachingIconView(Context context, @Nullable AttributeSet attrs) { 70 this(context, attrs, 0, 0); 71 } 72 CachingIconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)73 public CachingIconView(Context context, @Nullable AttributeSet attrs, 74 int defStyleAttr) { 75 this(context, attrs, defStyleAttr, 0); 76 } 77 CachingIconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)78 public CachingIconView(Context context, @Nullable AttributeSet attrs, 79 int defStyleAttr, int defStyleRes) { 80 super(context, attrs, defStyleAttr, defStyleRes); 81 init(context, attrs, defStyleAttr, defStyleRes); 82 } 83 init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)84 private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 85 int defStyleRes) { 86 if (attrs == null) { 87 return; 88 } 89 90 TypedArray ta = context.obtainStyledAttributes(attrs, 91 R.styleable.CachingIconView, defStyleAttr, defStyleRes); 92 mMaxDrawableWidth = ta.getDimensionPixelSize(R.styleable 93 .CachingIconView_maxDrawableWidth, -1); 94 mMaxDrawableHeight = ta.getDimensionPixelSize(R.styleable 95 .CachingIconView_maxDrawableHeight, -1); 96 ta.recycle(); 97 } 98 99 @Override 100 @RemotableViewMethod(asyncImpl="setImageIconAsync") setImageIcon(@ullable Icon icon)101 public void setImageIcon(@Nullable Icon icon) { 102 if (!testAndSetCache(icon)) { 103 mInternalSetDrawable = true; 104 // This calls back to setImageDrawable, make sure we don't clear the cache there. 105 Drawable drawable = loadSizeRestrictedIcon(icon); 106 if (drawable == null) { 107 super.setImageIcon(icon); 108 } else { 109 super.setImageDrawable(drawable); 110 } 111 mInternalSetDrawable = false; 112 } 113 } 114 115 @Nullable loadSizeRestrictedIcon(@ullable Icon icon)116 Drawable loadSizeRestrictedIcon(@Nullable Icon icon) { 117 return LocalImageResolver.resolveImage(icon, getContext(), mMaxDrawableWidth, 118 mMaxDrawableHeight); 119 } 120 121 @Override setImageIconAsync(@ullable final Icon icon)122 public Runnable setImageIconAsync(@Nullable final Icon icon) { 123 resetCache(); 124 Drawable drawable = loadSizeRestrictedIcon(icon); 125 if (drawable != null) { 126 return () -> setImageDrawable(drawable); 127 } 128 return super.setImageIconAsync(icon); 129 } 130 131 @Override 132 @RemotableViewMethod(asyncImpl="setImageResourceAsync") setImageResource(@rawableRes int resId)133 public void setImageResource(@DrawableRes int resId) { 134 if (!testAndSetCache(resId)) { 135 mInternalSetDrawable = true; 136 // This calls back to setImageDrawable, make sure we don't clear the cache there. 137 Drawable drawable = loadSizeRestrictedDrawable(resId); 138 if (drawable == null) { 139 super.setImageResource(resId); 140 } else { 141 super.setImageDrawable(drawable); 142 } 143 mInternalSetDrawable = false; 144 } 145 } 146 147 @Nullable loadSizeRestrictedDrawable(@rawableRes int resId)148 private Drawable loadSizeRestrictedDrawable(@DrawableRes int resId) { 149 return LocalImageResolver.resolveImage(resId, getContext(), mMaxDrawableWidth, 150 mMaxDrawableHeight); 151 } 152 153 @Override setImageResourceAsync(@rawableRes int resId)154 public Runnable setImageResourceAsync(@DrawableRes int resId) { 155 resetCache(); 156 Drawable drawable = loadSizeRestrictedDrawable(resId); 157 if (drawable != null) { 158 return () -> setImageDrawable(drawable); 159 } 160 161 return super.setImageResourceAsync(resId); 162 } 163 164 @Override 165 @RemotableViewMethod(asyncImpl="setImageURIAsync") setImageURI(@ullable Uri uri)166 public void setImageURI(@Nullable Uri uri) { 167 resetCache(); 168 Drawable drawable = loadSizeRestrictedUri(uri); 169 if (drawable == null) { 170 super.setImageURI(uri); 171 } else { 172 mInternalSetDrawable = true; 173 super.setImageDrawable(drawable); 174 mInternalSetDrawable = false; 175 } 176 } 177 178 @Nullable loadSizeRestrictedUri(@ullable Uri uri)179 private Drawable loadSizeRestrictedUri(@Nullable Uri uri) { 180 return LocalImageResolver.resolveImage(uri, getContext(), mMaxDrawableWidth, 181 mMaxDrawableHeight); 182 } 183 184 @Override setImageURIAsync(@ullable Uri uri)185 public Runnable setImageURIAsync(@Nullable Uri uri) { 186 resetCache(); 187 Drawable drawable = loadSizeRestrictedUri(uri); 188 if (drawable == null) { 189 return super.setImageURIAsync(uri); 190 } else { 191 return () -> setImageDrawable(drawable); 192 } 193 } 194 195 @Override setImageDrawable(@ullable Drawable drawable)196 public void setImageDrawable(@Nullable Drawable drawable) { 197 if (!mInternalSetDrawable) { 198 // Only clear the cache if we were externally called. 199 resetCache(); 200 } 201 super.setImageDrawable(drawable); 202 } 203 204 @Override 205 @RemotableViewMethod setImageBitmap(Bitmap bm)206 public void setImageBitmap(Bitmap bm) { 207 resetCache(); 208 super.setImageBitmap(bm); 209 } 210 211 @Override onConfigurationChanged(Configuration newConfig)212 protected void onConfigurationChanged(Configuration newConfig) { 213 super.onConfigurationChanged(newConfig); 214 resetCache(); 215 } 216 217 /** 218 * @return true if the currently set image is the same as {@param icon} 219 */ testAndSetCache(Icon icon)220 private synchronized boolean testAndSetCache(Icon icon) { 221 if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) { 222 String iconPackage = normalizeIconPackage(icon); 223 224 boolean isCached = mLastResId != 0 225 && icon.getResId() == mLastResId 226 && Objects.equals(iconPackage, mLastPackage); 227 228 mLastPackage = iconPackage; 229 mLastResId = icon.getResId(); 230 231 return isCached; 232 } else { 233 resetCache(); 234 return false; 235 } 236 } 237 238 /** 239 * @return true if the currently set image is the same as {@param resId} 240 */ testAndSetCache(int resId)241 private synchronized boolean testAndSetCache(int resId) { 242 boolean isCached; 243 if (resId == 0 || mLastResId == 0) { 244 isCached = false; 245 } else { 246 isCached = resId == mLastResId && null == mLastPackage; 247 } 248 mLastPackage = null; 249 mLastResId = resId; 250 return isCached; 251 } 252 253 /** 254 * Returns the normalized package name of {@param icon}. 255 * @return null if icon is null or if the icons package is null, empty or matches the current 256 * context. Otherwise returns the icon's package context. 257 */ normalizeIconPackage(Icon icon)258 private String normalizeIconPackage(Icon icon) { 259 if (icon == null) { 260 return null; 261 } 262 263 String pkg = icon.getResPackage(); 264 if (TextUtils.isEmpty(pkg)) { 265 return null; 266 } 267 if (pkg.equals(mContext.getPackageName())) { 268 return null; 269 } 270 return pkg; 271 } 272 resetCache()273 private synchronized void resetCache() { 274 mLastResId = 0; 275 mLastPackage = null; 276 } 277 278 /** 279 * Set the icon to be forcibly hidden, even when it's visibility is changed to visible. 280 * This is necessary since we still want to keep certain views hidden when their visibility 281 * is modified from other sources like the shelf. 282 */ setForceHidden(boolean forceHidden)283 public void setForceHidden(boolean forceHidden) { 284 if (forceHidden != mForceHidden) { 285 mForceHidden = forceHidden; 286 mWillBeForceHidden = false; 287 updateVisibility(); 288 if (mOnForceHiddenChangedListener != null) { 289 mOnForceHiddenChangedListener.accept(forceHidden); 290 } 291 } 292 } 293 294 @Override 295 @RemotableViewMethod setVisibility(int visibility)296 public void setVisibility(int visibility) { 297 mDesiredVisibility = visibility; 298 updateVisibility(); 299 } 300 updateVisibility()301 private void updateVisibility() { 302 int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE 303 : mDesiredVisibility; 304 if (mOnVisibilityChangedListener != null) { 305 mOnVisibilityChangedListener.accept(visibility); 306 } 307 super.setVisibility(visibility); 308 } 309 setOnVisibilityChangedListener(Consumer<Integer> listener)310 public void setOnVisibilityChangedListener(Consumer<Integer> listener) { 311 mOnVisibilityChangedListener = listener; 312 } 313 setOnForceHiddenChangedListener(Consumer<Boolean> listener)314 public void setOnForceHiddenChangedListener(Consumer<Boolean> listener) { 315 mOnForceHiddenChangedListener = listener; 316 } 317 318 isForceHidden()319 public boolean isForceHidden() { 320 return mForceHidden; 321 } 322 323 /** 324 * Provides the notification's background color to the icon. This is only used when the icon 325 * is "inverted". This should be called before calling {@link #setOriginalIconColor(int)}. 326 */ 327 @RemotableViewMethod setBackgroundColor(int color)328 public void setBackgroundColor(int color) { 329 mBackgroundColor = color; 330 } 331 332 /** 333 * Sets the icon color. If COLOR_INVALID is set, the icon's color filter will 334 * not be altered. If there is a background drawable, this method uses the value from 335 * {@link #setBackgroundColor(int)} which must have been already called. 336 */ 337 @RemotableViewMethod setOriginalIconColor(int color)338 public void setOriginalIconColor(int color) { 339 mIconColor = color; 340 Drawable background = getBackground(); 341 Drawable icon = getDrawable(); 342 boolean hasColor = color != ColoredIconHelper.COLOR_INVALID; 343 if (background == null) { 344 // This is the pre-S style -- colored icon with no background. 345 if (hasColor && icon != null) { 346 icon.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP); 347 } 348 } else { 349 // When there is a background drawable, color it with the foreground color and 350 // colorize the icon itself with the background color, creating an inverted effect. 351 if (hasColor) { 352 background.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP); 353 if (icon != null) { 354 icon.mutate().setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP); 355 } 356 } else { 357 background.mutate().setColorFilter(mBackgroundColor, PorterDuff.Mode.SRC_ATOP); 358 } 359 } 360 } 361 362 /** 363 * Set the icon's color filter: to gray if true, otherwise colored. 364 * If this icon has no original color, this has no effect. 365 */ setGrayedOut(boolean grayedOut)366 public void setGrayedOut(boolean grayedOut) { 367 // If there is a background drawable, then it has the foreground color and the image 368 // drawable has the background color, creating an inverted efffect. 369 Drawable drawable = getBackground(); 370 if (drawable == null) { 371 drawable = getDrawable(); 372 } 373 applyGrayTint(mContext, drawable, grayedOut, mIconColor); 374 } 375 getOriginalIconColor()376 public int getOriginalIconColor() { 377 return mIconColor; 378 } 379 380 /** 381 * @return if the view will be forceHidden after an animation 382 */ willBeForceHidden()383 public boolean willBeForceHidden() { 384 return mWillBeForceHidden; 385 } 386 387 /** 388 * Set that this view will be force hidden after an animation 389 * 390 * @param forceHidden if it will be forcehidden 391 */ setWillBeForceHidden(boolean forceHidden)392 public void setWillBeForceHidden(boolean forceHidden) { 393 mWillBeForceHidden = forceHidden; 394 } 395 396 /** 397 * Returns the set maximum width of drawable in pixels. -1 if not set. 398 */ getMaxDrawableWidth()399 public int getMaxDrawableWidth() { 400 return mMaxDrawableWidth; 401 } 402 403 /** 404 * Returns the set maximum height of drawable in pixels. -1 if not set. 405 */ getMaxDrawableHeight()406 public int getMaxDrawableHeight() { 407 return mMaxDrawableHeight; 408 } 409 } 410