Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d27d7fe0 authored by Jernej Virag's avatar Jernej Virag Committed by Android (Google) Code Review
Browse files

Merge changes from topic...

Merge changes from topic "revert-17199637-revert-17083643-large-icons-tm-XHQQYXZDHH-KIGUXQGTHZ" into tm-dev

* changes:
  Size restrict right notification icon size
  Downscale large bitmaps in CachingIconView
  Support downscaling of Drawable icons in LocalImageResolver
parents d079ef5d 5943bac5
Loading
Loading
Loading
Loading
+103 −7
Original line number Diff line number Diff line
@@ -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,8 @@ import android.view.RemotableViewMethod;
import android.widget.ImageView;
import android.widget.RemoteViews;

import com.android.internal.R;

import java.util.Objects;
import java.util.function.Consumer;

@@ -55,9 +58,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 +102,27 @@ public class CachingIconView extends ImageView {
        if (!testAndSetCache(icon)) {
            mInternalSetDrawable = true;
            // This calls back to setImageDrawable, make sure we don't clear the cache there.
            Drawable drawable = loadSizeRestrictedIcon(icon);
            if (drawable == null) {
                super.setImageIcon(icon);
            } else {
                super.setImageDrawable(drawable);
            }
            mInternalSetDrawable = false;
        }
    }

    @Nullable
    private Drawable loadSizeRestrictedIcon(@Nullable Icon icon) {
        return LocalImageResolver.resolveImage(icon, getContext(), mMaxDrawableWidth,
                mMaxDrawableHeight);
    }

    @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 +131,30 @@ public class CachingIconView extends ImageView {
        if (!testAndSetCache(resId)) {
            mInternalSetDrawable = true;
            // This calls back to setImageDrawable, make sure we don't clear the cache there.
            Drawable drawable = loadSizeRestrictedDrawable(resId);
            if (drawable == null) {
                super.setImageResource(resId);
            } else {
                super.setImageDrawable(drawable);
            }
            mInternalSetDrawable = false;
        }
    }

    @Nullable
    private Drawable loadSizeRestrictedDrawable(@DrawableRes int resId) {
        return LocalImageResolver.resolveImage(resId, getContext(), mMaxDrawableWidth,
                mMaxDrawableHeight);
    }

    @Override
    public Runnable setImageResourceAsync(@DrawableRes int resId) {
        resetCache();
        Drawable drawable = loadSizeRestrictedDrawable(resId);
        if (drawable != null) {
            return () -> setImageDrawable(drawable);
        }

        return super.setImageResourceAsync(resId);
    }

@@ -98,13 +162,31 @@ public class CachingIconView extends ImageView {
    @RemotableViewMethod(asyncImpl="setImageURIAsync")
    public void setImageURI(@Nullable Uri uri) {
        resetCache();
        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) {
        return LocalImageResolver.resolveImage(uri, getContext(), mMaxDrawableWidth,
                mMaxDrawableHeight);
    }

    @Override
    public Runnable setImageURIAsync(@Nullable Uri uri) {
        resetCache();
        Drawable drawable = loadSizeRestrictedUri(uri);
        if (drawable == null) {
            return super.setImageURIAsync(uri);
        } else {
            return () -> setImageDrawable(drawable);
        }
    }

    @Override
@@ -307,4 +389,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;
    }
}
+164 −38
Original line number Diff line number Diff line
@@ -16,57 +16,174 @@

package com.android.internal.widget;

import android.annotation.DrawableRes;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
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.Log;
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;
    private static final String TAG = "LocalImageResolver";

    @VisibleForTesting
    static final int DEFAULT_MAX_SAFE_ICON_SIZE_PX = 480;

    /**
     * Resolve an image from the given Uri using {@link ImageDecoder}
     * Resolve an image from the given Uri using {@link ImageDecoder} if it contains a
     * bitmap reference.
     */
    @Nullable
    public static Drawable resolveImage(Uri uri, Context context) throws IOException {
        try {
            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));
        } catch (Exception e) {
            // Invalid drawable resource can actually throw either NullPointerException or
            // ResourceNotFoundException. This sanitizes to expected output.
            throw new IOException(e);
        }
    }

    /**
     * Get the drawable from Icon using {@link ImageDecoder} if it contains a Uri, or
     * Get the drawable from Icon using {@link ImageDecoder} if it contains a bitmap reference, or
     * using {@link Icon#loadDrawable(Context)} otherwise.  This will correctly apply the Icon's,
     * tint, if present, to the drawable.
     *
     * @return drawable or null if loading failed.
     */
    public static Drawable resolveImage(Icon icon, Context context) throws IOException {
    @Nullable
    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 bitmap reference, or
     * using {@link Icon#loadDrawable(Context)} otherwise.  This will correctly apply the Icon's,
     * tint, if present, to the drawable.
     *
     * @throws IOException if the icon could not be loaded for whichever reason
     */
    @Nullable
    public static Drawable resolveImage(@Nullable Icon icon, Context context, int maxWidth,
            int maxHeight) {
        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);
            if (icon.hasTint()) {
                result.mutate();
                result.setTintList(icon.getTintList());
                result.setTintBlendMode(icon.getTintBlendMode());
                    Drawable result = resolveImage(uri, context, maxWidth, maxHeight);
                    if (result != null) {
                        return tintDrawable(icon, result);
                    }
            return 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.
        try {
            return icon.loadDrawable(context);
        } catch (Resources.NotFoundException e) {
            return null;
        }
    }

    public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight)
            throws IOException {
    /**
     * Attempts to resolve the resource as a bitmap drawable constrained within max sizes.
     */
    @Nullable
    public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight) {
        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) {
        final ImageDecoder.Source source = ImageDecoder.createSource(context.getResources(), resId);
        return resolveImage(source, maxWidth, maxHeight);
    }

    @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) {
        try {
            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) {
@@ -80,6 +197,14 @@ public class LocalImageResolver {
                    }
                }
            });

        // ImageDecoder documentation is misleading a bit - it'll throw NotFoundException
        // in some cases despite it not saying so. Rethrow it as an IOException to keep
        // our API contract.
        } catch (IOException | Resources.NotFoundException e) {
            Log.e(TAG, "Failed to load image drawable", e);
            return null;
        }
    }

    private static int getPowerOfTwoForSampleRatio(double ratio) {
@@ -88,11 +213,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));
    }
@@ -101,7 +227,7 @@ public class LocalImageResolver {
     * Gets the Uri for this icon, assuming the icon can be treated as a pure Uri.  Null otherwise.
     */
    @Nullable
    public static Uri getResolvableUri(@Nullable Icon icon) {
    private static Uri getResolvableUri(@Nullable Icon icon) {
        if (icon == null || (icon.getType() != Icon.TYPE_URI
                && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
            return null;
+2 −0
Original line number Diff line number Diff line
@@ -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 -->
+5 −1
Original line number Diff line number Diff line
@@ -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
+3 −1
Original line number Diff line number Diff line
@@ -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"
    />
Loading