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

Commit 6f2b34ba authored by Jernej Virag's avatar Jernej Virag
Browse files

Downscale large bitmaps in CachingIconView

CachingIconView is used to displayed (smallish) icons in Notifications. Those can accidentally be made very large if big resources are used.
This makes CachingIconView use LocalImageResolver to load those images with limited size. This fixes large memory use of notification header icons.

Bug: 210690571
Bug: 218845090

Test: atest CachingIconViewTest
      atest NotificationManagerTest
      Verified with notification test APK on a Pixel

Change-Id: Ia37c1fd31a7720e6ec2fd20d7466d8dcaffa6507
parent 69832591
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;
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -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>
+4 −0
Original line number Diff line number Diff line
@@ -147,6 +147,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">
+24 −0
Original line number Diff line number Diff line
<?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" />
+22 −0
Original line number Diff line number Diff line
<?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" />
Loading