Merge changes from topic "large-icons-tm" into tm-dev

* changes:
  Size restrict right notification icon size
  Downscale large bitmaps in CachingIconView
  Support downscaling of Drawable icons in LocalImageResolver
diff --git a/core/java/com/android/internal/widget/CachingIconView.java b/core/java/com/android/internal/widget/CachingIconView.java
index 299cbe1..bd27e60 100644
--- a/core/java/com/android/internal/widget/CachingIconView.java
+++ b/core/java/com/android/internal/widget/CachingIconView.java
@@ -23,6 +23,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
@@ -35,6 +36,9 @@
 import android.widget.ImageView;
 import android.widget.RemoteViews;
 
+import com.android.internal.R;
+
+import java.io.IOException;
 import java.util.Objects;
 import java.util.function.Consumer;
 
@@ -55,9 +59,42 @@
     private int mBackgroundColor;
     private boolean mWillBeForceHidden;
 
+    private int mMaxDrawableWidth = -1;
+    private int mMaxDrawableHeight = -1;
+
+    public CachingIconView(Context context) {
+        this(context, null, 0, 0);
+    }
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public CachingIconView(Context context, @Nullable AttributeSet attrs) {
-        super(context, attrs);
+        this(context, attrs, 0, 0);
+    }
+
+    public CachingIconView(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CachingIconView(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        if (attrs == null) {
+            return;
+        }
+
+        TypedArray ta = context.obtainStyledAttributes(attrs,
+                R.styleable.CachingIconView, defStyleAttr, defStyleRes);
+        mMaxDrawableWidth = ta.getDimensionPixelSize(R.styleable
+                .CachingIconView_maxDrawableWidth, -1);
+        mMaxDrawableHeight = ta.getDimensionPixelSize(R.styleable
+                .CachingIconView_maxDrawableHeight, -1);
+        ta.recycle();
     }
 
     @Override
@@ -66,15 +103,31 @@
         if (!testAndSetCache(icon)) {
             mInternalSetDrawable = true;
             // This calls back to setImageDrawable, make sure we don't clear the cache there.
-            super.setImageIcon(icon);
+            Drawable drawable = loadSizeRestrictedIcon(icon);
+            if (drawable == null) {
+                super.setImageIcon(icon);
+            } else {
+                super.setImageDrawable(drawable);
+            }
             mInternalSetDrawable = false;
         }
     }
 
+    @Nullable
+    private Drawable loadSizeRestrictedIcon(@Nullable Icon icon) {
+        try {
+            return LocalImageResolver.resolveImage(icon, getContext(), mMaxDrawableWidth,
+                    mMaxDrawableHeight);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     @Override
-    public Runnable setImageIconAsync(@Nullable Icon icon) {
+    public Runnable setImageIconAsync(@Nullable final Icon icon) {
         resetCache();
-        return super.setImageIconAsync(icon);
+        Drawable drawable = loadSizeRestrictedIcon(icon);
+        return () -> setImageDrawable(drawable);
     }
 
     @Override
@@ -83,14 +136,34 @@
         if (!testAndSetCache(resId)) {
             mInternalSetDrawable = true;
             // This calls back to setImageDrawable, make sure we don't clear the cache there.
-            super.setImageResource(resId);
+            Drawable drawable = loadSizeRestrictedDrawable(resId);
+            if (drawable == null) {
+                super.setImageResource(resId);
+            } else {
+                super.setImageDrawable(drawable);
+            }
             mInternalSetDrawable = false;
         }
     }
 
+    @Nullable
+    private Drawable loadSizeRestrictedDrawable(@DrawableRes int resId) {
+        try {
+            return LocalImageResolver.resolveImage(resId, getContext(), mMaxDrawableWidth,
+                    mMaxDrawableHeight);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     @Override
     public Runnable setImageResourceAsync(@DrawableRes int resId) {
         resetCache();
+        Drawable drawable = loadSizeRestrictedDrawable(resId);
+        if (drawable != null) {
+            return () -> setImageDrawable(drawable);
+        }
+
         return super.setImageResourceAsync(resId);
     }
 
@@ -98,13 +171,35 @@
     @RemotableViewMethod(asyncImpl="setImageURIAsync")
     public void setImageURI(@Nullable Uri uri) {
         resetCache();
-        super.setImageURI(uri);
+        Drawable drawable = loadSizeRestrictedUri(uri);
+        if (drawable == null) {
+            super.setImageURI(uri);
+        } else {
+            mInternalSetDrawable = true;
+            super.setImageDrawable(drawable);
+            mInternalSetDrawable = false;
+        }
+    }
+
+    @Nullable
+    private Drawable loadSizeRestrictedUri(@Nullable Uri uri) {
+        try {
+            return LocalImageResolver.resolveImage(uri, getContext(), mMaxDrawableWidth,
+                    mMaxDrawableHeight);
+        } catch (IOException e) {
+            return null;
+        }
     }
 
     @Override
     public Runnable setImageURIAsync(@Nullable Uri uri) {
         resetCache();
-        return super.setImageURIAsync(uri);
+        Drawable drawable = loadSizeRestrictedUri(uri);
+        if (drawable == null) {
+            return super.setImageURIAsync(uri);
+        } else {
+            return () -> setImageDrawable(drawable);
+        }
     }
 
     @Override
@@ -307,4 +402,18 @@
     public void setWillBeForceHidden(boolean forceHidden) {
         mWillBeForceHidden = forceHidden;
     }
+
+    /**
+     * Returns the set maximum width of drawable in pixels. -1 if not set.
+     */
+    public int getMaxDrawableWidth() {
+        return mMaxDrawableWidth;
+    }
+
+    /**
+     * Returns the set maximum height of drawable in pixels. -1 if not set.
+     */
+    public int getMaxDrawableHeight() {
+        return mMaxDrawableHeight;
+    }
 }
diff --git a/core/java/com/android/internal/widget/LocalImageResolver.java b/core/java/com/android/internal/widget/LocalImageResolver.java
index 616b699..66a3ff9 100644
--- a/core/java/com/android/internal/widget/LocalImageResolver.java
+++ b/core/java/com/android/internal/widget/LocalImageResolver.java
@@ -16,21 +16,25 @@
 
 package com.android.internal.widget;
 
+import android.annotation.DrawableRes;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.ImageDecoder;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.util.Size;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.IOException;
 
 /** A class to extract Drawables from a MessagingStyle/ConversationStyle message. */
 public class LocalImageResolver {
-    private static final String TAG = LocalImageResolver.class.getSimpleName();
 
-    private static final int MAX_SAFE_ICON_SIZE_PX = 480;
+    @VisibleForTesting
+    static final int DEFAULT_MAX_SAFE_ICON_SIZE_PX = 480;
 
     /**
      * Resolve an image from the given Uri using {@link ImageDecoder}
@@ -38,9 +42,9 @@
     public static Drawable resolveImage(Uri uri, Context context) throws IOException {
         final ImageDecoder.Source source =
                 ImageDecoder.createSource(context.getContentResolver(), uri);
-        final Drawable drawable =
-                ImageDecoder.decodeDrawable(source, LocalImageResolver::onHeaderDecoded);
-        return drawable;
+        return ImageDecoder.decodeDrawable(source,
+                (decoder, info, s) -> LocalImageResolver.onHeaderDecoded(decoder, info,
+                        DEFAULT_MAX_SAFE_ICON_SIZE_PX, DEFAULT_MAX_SAFE_ICON_SIZE_PX));
     }
 
     /**
@@ -48,17 +52,49 @@
      * using {@link Icon#loadDrawable(Context)} otherwise.  This will correctly apply the Icon's,
      * tint, if present, to the drawable.
      */
-    public static Drawable resolveImage(Icon icon, Context context) throws IOException {
-        Uri uri = getResolvableUri(icon);
-        if (uri != null) {
-            Drawable result = resolveImage(uri, context);
-            if (icon.hasTint()) {
-                result.mutate();
-                result.setTintList(icon.getTintList());
-                result.setTintBlendMode(icon.getTintBlendMode());
-            }
-            return result;
+    public static Drawable resolveImage(@Nullable Icon icon, Context context)
+            throws IOException {
+        return resolveImage(icon, context, DEFAULT_MAX_SAFE_ICON_SIZE_PX,
+                DEFAULT_MAX_SAFE_ICON_SIZE_PX);
+    }
+
+    /**
+     * Get the drawable from Icon using {@link ImageDecoder} if it contains a Uri, or
+     * using {@link Icon#loadDrawable(Context)} otherwise.  This will correctly apply the Icon's,
+     * tint, if present, to the drawable.
+     */
+    @Nullable
+    public static Drawable resolveImage(@Nullable Icon icon, Context context, int maxWidth,
+            int maxHeight)
+            throws IOException {
+        if (icon == null) {
+            return null;
         }
+
+        switch (icon.getType()) {
+            case Icon.TYPE_URI:
+            case Icon.TYPE_URI_ADAPTIVE_BITMAP:
+                Uri uri = getResolvableUri(icon);
+                if (uri != null) {
+                    Drawable result = resolveImage(uri, context, maxWidth, maxHeight);
+                    return tintDrawable(icon, result);
+                }
+                break;
+            case Icon.TYPE_RESOURCE:
+                Drawable result = resolveImage(icon.getResId(), context, maxWidth, maxHeight);
+                if (result != null) {
+                    return tintDrawable(icon, result);
+                }
+                break;
+            case Icon.TYPE_BITMAP:
+            case Icon.TYPE_ADAPTIVE_BITMAP:
+                return resolveBitmapImage(icon, context, maxWidth, maxHeight);
+            case Icon.TYPE_DATA:    // We can't really improve on raw data images.
+            default:
+                break;
+        }
+
+        // Fallback to straight drawable load if we fail with more efficient approach.
         return icon.loadDrawable(context);
     }
 
@@ -66,7 +102,71 @@
             throws IOException {
         final ImageDecoder.Source source =
                 ImageDecoder.createSource(context.getContentResolver(), uri);
+        return resolveImage(source, maxWidth, maxHeight);
+    }
+
+    /**
+     * Attempts to resolve the resource as a bitmap drawable constrained within max sizes.
+     *
+     * @return decoded drawable or null if the passed resource is not a straight bitmap
+     */
+    @Nullable
+    public static Drawable resolveImage(@DrawableRes int resId, Context context, int maxWidth,
+            int maxHeight)
+            throws IOException {
+        final ImageDecoder.Source source = ImageDecoder.createSource(context.getResources(), resId);
+        // It's possible that the resource isn't an actual bitmap drawable so this decode can fail.
+        // Return null in that case.
+        try {
+            return resolveImage(source, maxWidth, maxHeight);
+        } catch (ImageDecoder.DecodeException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    private static Drawable resolveBitmapImage(Icon icon, Context context, int maxWidth,
+            int maxHeight) {
+        Bitmap bitmap = icon.getBitmap();
+        if (bitmap == null) {
+            return null;
+        }
+
+        if (bitmap.getWidth() > maxWidth || bitmap.getHeight() > maxHeight) {
+            Icon smallerIcon = icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP
+                    ? Icon.createWithAdaptiveBitmap(bitmap) : Icon.createWithBitmap(bitmap);
+            // We don't want to modify the source icon, create a copy.
+            smallerIcon.setTintList(icon.getTintList())
+                    .setTintBlendMode(icon.getTintBlendMode())
+                    .scaleDownIfNecessary(maxWidth, maxHeight);
+            return smallerIcon.loadDrawable(context);
+        }
+
+        return icon.loadDrawable(context);
+    }
+
+    @Nullable
+    private static Drawable tintDrawable(Icon icon, @Nullable Drawable drawable) {
+        if (drawable == null) {
+            return null;
+        }
+
+        if (icon.hasTint()) {
+            drawable.mutate();
+            drawable.setTintList(icon.getTintList());
+            drawable.setTintBlendMode(icon.getTintBlendMode());
+        }
+
+        return drawable;
+    }
+
+    private static Drawable resolveImage(ImageDecoder.Source source, int maxWidth, int maxHeight)
+            throws IOException {
         return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> {
+            if (maxWidth <= 0 || maxHeight <= 0) {
+                return;
+            }
+
             final Size size = info.getSize();
             if (size.getWidth() > size.getHeight()) {
                 if (size.getWidth() > maxWidth) {
@@ -88,11 +188,12 @@
     }
 
     private static void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
-            ImageDecoder.Source source) {
+            int maxWidth, int maxHeight) {
         final Size size = info.getSize();
         final int originalSize = Math.max(size.getHeight(), size.getWidth());
-        final double ratio = (originalSize > MAX_SAFE_ICON_SIZE_PX)
-                ? originalSize * 1f / MAX_SAFE_ICON_SIZE_PX
+        final int maxSize = Math.max(maxWidth, maxHeight);
+        final double ratio = (originalSize > maxSize)
+                ? originalSize * 1f / maxSize
                 : 1.0;
         decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio));
     }
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index 81a79c5..a7f2aa7 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -49,6 +49,8 @@
         android:layout_marginStart="@dimen/notification_icon_circle_start"
         android:background="@drawable/notification_icon_circle"
         android:padding="@dimen/notification_icon_circle_padding"
+        android:maxDrawableWidth="@dimen/notification_icon_circle_size"
+        android:maxDrawableHeight="@dimen/notification_icon_circle_size"
         />
 
     <!-- extends ViewGroup -->
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index c6983ae..fd787f6 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -45,6 +45,8 @@
         android:layout_marginStart="@dimen/notification_icon_circle_start"
         android:background="@drawable/notification_icon_circle"
         android:padding="@dimen/notification_icon_circle_padding"
+        android:maxDrawableWidth="@dimen/notification_icon_circle_size"
+        android:maxDrawableHeight="@dimen/notification_icon_circle_size"
         />
 
     <FrameLayout
@@ -136,7 +138,7 @@
 
         </LinearLayout>
 
-        <ImageView
+        <com.android.internal.widget.CachingIconView
             android:id="@+id/right_icon"
             android:layout_width="@dimen/notification_right_icon_size"
             android:layout_height="@dimen/notification_right_icon_size"
@@ -148,6 +150,8 @@
             android:clipToOutline="true"
             android:importantForAccessibility="no"
             android:scaleType="centerCrop"
+            android:maxDrawableWidth="@dimen/notification_right_icon_size"
+            android:maxDrawableHeight="@dimen/notification_right_icon_size"
             />
 
         <FrameLayout
diff --git a/core/res/res/layout/notification_template_right_icon.xml b/core/res/res/layout/notification_template_right_icon.xml
index f163ed5..8b3b795 100644
--- a/core/res/res/layout/notification_template_right_icon.xml
+++ b/core/res/res/layout/notification_template_right_icon.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<ImageView
+<com.android.internal.widget.CachingIconView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/right_icon"
     android:layout_width="@dimen/notification_right_icon_size"
@@ -25,4 +25,6 @@
     android:clipToOutline="true"
     android:importantForAccessibility="no"
     android:scaleType="centerCrop"
+    android:maxDrawableWidth="@dimen/notification_right_icon_size"
+    android:maxDrawableHeight="@dimen/notification_right_icon_size"
     />
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 2107f65..b3203ae 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -9807,4 +9807,12 @@
         of the supported locale. {@link android.app.LocaleConfig} -->
         <attr name="name" />
     </declare-styleable>
+
+    <!-- @hide -->
+    <declare-styleable name="CachingIconView">
+        <!-- Maximum width of displayed drawable. Drawables exceeding this size will be downsampled. -->
+        <attr name="maxDrawableWidth" format="dimension"/>
+        <!-- Maximum width of height drawable. Drawables exceeding this size will be downsampled. -->
+        <attr name="maxDrawableHeight" format="dimension"/>
+    </declare-styleable>
     </resources>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 2dc17b8..0a4c4c0 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -148,6 +148,10 @@
     <public name="supportsInlineSuggestionsWithTouchExploration" />
     <public name="lineBreakStyle" />
     <public name="lineBreakWordStyle" />
+    <!-- @hide -->
+    <public name="maxDrawableWidth" />
+    <!-- @hide -->
+    <public name="maxDrawableHeight" />
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01de0000">
diff --git a/core/tests/coretests/res/drawable/big_a.png b/core/tests/coretests/res/drawable/big_a.png
new file mode 100644
index 0000000..dc059a3
--- /dev/null
+++ b/core/tests/coretests/res/drawable/big_a.png
Binary files differ
diff --git a/core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml b/core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml
new file mode 100644
index 0000000..9a03446
--- /dev/null
+++ b/core/tests/coretests/res/layout/caching_icon_view_test_max_size.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.internal.widget.CachingIconView
+          xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/caching_icon_view"
+          android:layout_width="120dp"
+          android:layout_height="120dp"
+          android:maxDrawableWidth="80dp"
+          android:maxDrawableHeight="80dp" />
diff --git a/core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml b/core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml
new file mode 100644
index 0000000..a213a97
--- /dev/null
+++ b/core/tests/coretests/res/layout/caching_icon_view_test_no_max_size.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.internal.widget.CachingIconView
+          xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/caching_icon_view"
+          android:layout_width="120dp"
+          android:layout_height="120dp" />
diff --git a/core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java b/core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java
new file mode 100644
index 0000000..0d4b449
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/CachingIconViewTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.InsetDrawable;
+import android.net.Uri;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CachingIconViewTest {
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    @Test
+    public void customDrawable_setImageIcon_skipsResizeSuccessfully() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageIcon(Icon.createWithResource(mContext, R.drawable.custom_drawable));
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(InsetDrawable.class);
+    }
+
+    @Test
+    public void customDrawable_setImageIconAsync_skipsResizeSuccessfully() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageIconAsync(Icon.createWithResource(mContext, R.drawable.custom_drawable)).run();
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(InsetDrawable.class);
+    }
+
+    @Test
+    public void customDrawable_setImageResource_skipsResizeSuccessfully() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageResource(R.drawable.custom_drawable);
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(InsetDrawable.class);
+    }
+
+    @Test
+    public void customDrawable_setImageResourceAsync_skipsResizeSuccessfully() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageResourceAsync(R.drawable.custom_drawable).run();
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(InsetDrawable.class);
+    }
+
+    @Test
+    public void customDrawable_setImageUri_skipsResizeSuccessfully() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageURI(Uri.parse(
+                "android.resource://com.android.frameworks.coretests/"
+                        + R.drawable.custom_drawable));
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(InsetDrawable.class);
+    }
+
+    @Test
+    public void customDrawable_setImageUriAsync_skipsResizeSuccessfully() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageURIAsync(Uri.parse(
+                "android.resource://com.android.frameworks.coretests/"
+                        + R.drawable.custom_drawable)).run();
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(InsetDrawable.class);
+    }
+
+    @Test
+    public void maxDrawableDimensionsSet_setImageIcon_resizesImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageIcon(Icon.createWithResource(mContext, R.drawable.big_a));
+
+        assertDrawableResized(view);
+    }
+
+    @Test
+    public void maxDrawableWithNoDimensionsSet_setImageIcon_doesNotResizeImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_no_max_size, null);
+        view.setImageIcon(Icon.createWithResource(mContext, R.drawable.big_a));
+
+        assertDrawableNotResized(view);
+    }
+
+    @Test
+    public void maxDrawableDimensionsSet_setImageIconAsync_resizesImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageIconAsync(Icon.createWithResource(mContext, R.drawable.big_a)).run();
+
+        assertDrawableResized(view);
+    }
+
+    @Test
+    public void maxDrawableWithNoDimensionsSet_setImageIconAsync_doesNotResizeImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_no_max_size, null);
+        view.setImageIconAsync(Icon.createWithResource(mContext, R.drawable.big_a)).run();
+
+        assertDrawableNotResized(view);
+    }
+
+    @Test
+    public void maxDrawableDimensionsSet_setImageResource_resizesImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageResource(R.drawable.big_a);
+
+        assertDrawableResized(view);
+    }
+
+    @Test
+    public void maxDrawableWithNoDimensionsSet_setImageResource_doesNotResizeImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_no_max_size, null);
+        view.setImageResource(R.drawable.big_a);
+
+        assertDrawableNotResized(view);
+    }
+
+    @Test
+    public void maxDrawableDimensionsSet_setImageResourceAsync_resizesImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageResourceAsync(R.drawable.big_a).run();
+
+        assertDrawableResized(view);
+    }
+
+    @Test
+    public void maxDrawableWithNoDimensionsSet_setImageResourceAsync_doesNotResizeImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_no_max_size, null);
+        view.setImageResourceAsync(R.drawable.big_a).run();
+
+        assertDrawableNotResized(view);
+    }
+
+    @Test
+    public void maxDrawableDimensionsSet_setImageUri_resizesImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageURI(Uri.parse(
+                "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a));
+
+        assertDrawableResized(view);
+    }
+
+    @Test
+    public void maxDrawableWithNoDimensionsSet_setImageUri_doesNotResizeImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_no_max_size, null);
+        view.setImageURI(Uri.parse(
+                "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a));
+
+        assertDrawableNotResized(view);
+    }
+
+    @Test
+    public void maxDrawableDimensionsSet_setImageUriAsync_resizesImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_max_size, null);
+        view.setImageURIAsync(Uri.parse(
+                "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a)).run();
+
+        assertDrawableResized(view);
+    }
+
+    @Test
+    public void maxDrawableWithNoDimensionsSet_setImageUriAsync_doesNotResizeImageIcon() {
+        CachingIconView view = (CachingIconView) LayoutInflater.from(mContext).inflate(
+                R.layout.caching_icon_view_test_no_max_size, null);
+        view.setImageURIAsync(Uri.parse(
+                "android.resource://com.android.frameworks.coretests/" + R.drawable.big_a)).run();
+
+        assertDrawableNotResized(view);
+    }
+
+
+    private void assertDrawableResized(@Nullable CachingIconView view) {
+        assertThat(view).isNotNull();
+        int maxSize =
+                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f,
+                        mContext.getResources().getDisplayMetrics());
+        assertThat(view.getMaxDrawableHeight()).isEqualTo(maxSize);
+        assertThat(view.getMaxDrawableWidth()).isEqualTo(maxSize);
+
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+        assertThat(bitmapDrawable.getBitmap().getWidth()).isLessThan(maxSize + 1);
+        assertThat(bitmapDrawable.getBitmap().getHeight()).isLessThan(maxSize + 1);
+    }
+
+    private void assertDrawableNotResized(@Nullable CachingIconView view) {
+        assertThat(view).isNotNull();
+        int maxSize =
+                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f,
+                        mContext.getResources().getDisplayMetrics());
+        assertThat(view.getMaxDrawableHeight()).isEqualTo(-1);
+        assertThat(view.getMaxDrawableWidth()).isEqualTo(-1);
+
+        Drawable drawable = view.getDrawable();
+        assertThat(drawable).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+        assertThat(bitmapDrawable.getBitmap().getWidth()).isGreaterThan(maxSize);
+        assertThat(bitmapDrawable.getBitmap().getHeight()).isGreaterThan(maxSize);
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java b/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java
new file mode 100644
index 0000000..8dcb4a2
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.frameworks.coretests.R;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class LocalImageResolverTest {
+
+    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+    @Test
+    public void resolveImage_largeBitmapIcon_defaultSize_resizeToDefaultSize() throws
+            IOException {
+        Icon icon = Icon.createWithBitmap(
+                BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext);
+
+        assertThat(d).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) d;
+        // No isLessOrEqualThan sadly.
+        assertThat(bd.getBitmap().getWidth()).isLessThan(
+                LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
+        assertThat(bd.getBitmap().getHeight()).isLessThan(
+                LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
+    }
+
+    @Test
+    public void resolveImage_largeAdaptiveBitmapIcon_defaultSize_resizeToDefaultSize() throws
+            IOException {
+        Icon icon = Icon.createWithAdaptiveBitmap(
+                BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext);
+
+        assertThat(d).isInstanceOf(AdaptiveIconDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) ((AdaptiveIconDrawable) d).getForeground();
+        // No isLessOrEqualThan sadly.
+        assertThat(bd.getBitmap().getWidth()).isLessThan(
+                LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
+        assertThat(bd.getBitmap().getHeight()).isLessThan(
+                LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
+    }
+
+    @Test
+    public void resolveImage_largeResourceIcon_defaultSize_resizeToDefaultSize() throws
+            IOException {
+        Icon icon = Icon.createWithResource(mContext, R.drawable.big_a);
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext);
+
+        assertThat(d).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) d;
+        // No isLessOrEqualThan sadly.
+        assertThat(bd.getBitmap().getWidth()).isLessThan(
+                LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
+        assertThat(bd.getBitmap().getHeight()).isLessThan(
+                LocalImageResolver.DEFAULT_MAX_SAFE_ICON_SIZE_PX + 1);
+    }
+
+    @Test
+    public void resolveImage_largeResourceIcon_passedSize_resizeToDefinedSize() throws
+            IOException {
+        Icon icon = Icon.createWithResource(mContext, R.drawable.big_a);
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext, 100, 50);
+
+        assertThat(d).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) d;
+        assertThat(bd.getBitmap().getWidth()).isLessThan(101);
+        assertThat(bd.getBitmap().getHeight()).isLessThan(51);
+    }
+
+    @Test
+    public void resolveImage_largeBitmapIcon_passedSize_resizeToDefinedSize() throws
+            IOException {
+        Icon icon = Icon.createWithBitmap(
+                BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext, 100, 50);
+
+        assertThat(d).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) d;
+        assertThat(bd.getBitmap().getWidth()).isLessThan(101);
+        assertThat(bd.getBitmap().getHeight()).isLessThan(51);
+    }
+
+    @Test
+    public void resolveImage_largeAdaptiveBitmapIcon_passedSize_resizeToDefinedSize() throws
+            IOException {
+        Icon icon = Icon.createWithAdaptiveBitmap(
+                BitmapFactory.decodeResource(mContext.getResources(), R.drawable.big_a));
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext, 100, 50);
+
+        assertThat(d).isInstanceOf(AdaptiveIconDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) ((AdaptiveIconDrawable) d).getForeground();
+        assertThat(bd.getBitmap().getWidth()).isLessThan(101);
+        assertThat(bd.getBitmap().getHeight()).isLessThan(51);
+    }
+
+
+    @Test
+    public void resolveImage_smallResourceIcon_defaultSize_untouched() throws IOException {
+        Icon icon = Icon.createWithResource(mContext, R.drawable.test32x24);
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext);
+
+        assertThat(d).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) d;
+        assertThat(bd.getBitmap().getWidth()).isEqualTo(32);
+        assertThat(bd.getBitmap().getHeight()).isEqualTo(24);
+    }
+
+    @Test
+    public void resolveImage_smallBitmapIcon_defaultSize_untouched() throws IOException {
+        Icon icon = Icon.createWithBitmap(
+                BitmapFactory.decodeResource(mContext.getResources(), R.drawable.test32x24));
+        final int originalWidth = icon.getBitmap().getWidth();
+        final int originalHeight = icon.getBitmap().getHeight();
+
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext);
+
+        assertThat(d).isInstanceOf(BitmapDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) d;
+        assertThat(bd.getBitmap().getWidth()).isEqualTo(originalWidth);
+        assertThat(bd.getBitmap().getHeight()).isEqualTo(originalHeight);
+    }
+
+    @Test
+    public void resolveImage_smallAdaptiveBitmapIcon_defaultSize_untouched() throws IOException {
+        Icon icon = Icon.createWithAdaptiveBitmap(
+                BitmapFactory.decodeResource(mContext.getResources(), R.drawable.test32x24));
+        final int originalWidth = icon.getBitmap().getWidth();
+        final int originalHeight = icon.getBitmap().getHeight();
+
+        Drawable d = LocalImageResolver.resolveImage(icon, mContext);
+        assertThat(d).isInstanceOf(AdaptiveIconDrawable.class);
+        BitmapDrawable bd = (BitmapDrawable) ((AdaptiveIconDrawable) d).getForeground();
+        assertThat(bd.getBitmap().getWidth()).isEqualTo(originalWidth);
+        assertThat(bd.getBitmap().getHeight()).isEqualTo(originalHeight);
+
+    }
+}