diff options
12 files changed, 722 insertions, 27 deletions
diff --git a/core/java/com/android/internal/widget/CachingIconView.java b/core/java/com/android/internal/widget/CachingIconView.java index 299cbe12b4d1..bd27e60f7199 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.annotation.Nullable;  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.view.RemotableViewMethod;  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 @@ public class CachingIconView extends ImageView {      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 @@ public class CachingIconView extends ImageView {          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 @@ public class CachingIconView extends ImageView {          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 @@ public class CachingIconView extends ImageView {      @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 class CachingIconView extends ImageView {      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 616b69961b79..66a3ff950577 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 class LocalImageResolver {      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 @@ public class LocalImageResolver {       * 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 @@ public class LocalImageResolver {              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 @@ public class LocalImageResolver {      }      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 81a79c50c3ef..a7f2aa7cba69 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 c6983ae5e045..fd787f6ea470 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 f163ed5f955a..8b3b795f7473 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 2107f651eade..b3203aebcb99 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 2dc17b8468c3..0a4c4c0cbb8f 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 Binary files differnew file mode 100644 index 000000000000..dc059a3557a8 --- /dev/null +++ b/core/tests/coretests/res/drawable/big_a.png 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 000000000000..9a034466b0fd --- /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 000000000000..a213a977761d --- /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 000000000000..0d4b4495578b --- /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 000000000000..8dcb4a27b24d --- /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); + +    } +}  |