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

Commit a2be7ce3 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "AnimatedImageDrawable in MessageStyle+ConversationStyle"

parents 73e94c4c 75c0c152
Loading
Loading
Loading
Loading
+36 −44
Original line number Original line Diff line number Diff line
@@ -16,69 +16,61 @@


package com.android.internal.widget;
package com.android.internal.widget;


import android.annotation.Nullable;
import android.content.Context;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.ImageDecoder;
import android.graphics.BitmapFactory;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.Uri;
import android.util.Log;
import android.util.Size;


import java.io.IOException;
import java.io.IOException;
import java.io.InputStream;


/**
/** A class to extract Drawables from a MessagingStyle/ConversationStyle message. */
 * A class to extract Bitmaps from a MessagingStyle message.
 */
public class LocalImageResolver {
public class LocalImageResolver {
    private static final String TAG = LocalImageResolver.class.getSimpleName();
    private static final String TAG = LocalImageResolver.class.getSimpleName();


    private static final int MAX_SAFE_ICON_SIZE_PX = 480;
    private static final int MAX_SAFE_ICON_SIZE_PX = 480;


    @Nullable
    public static Drawable resolveImage(Uri uri, Context context) throws IOException {
    public static Drawable resolveImage(Uri uri, Context context) throws IOException {
        BitmapFactory.Options onlyBoundsOptions = getBoundsOptionsForImage(uri, context);
        final ImageDecoder.Source source =
        if ((onlyBoundsOptions.outWidth == -1) || (onlyBoundsOptions.outHeight == -1)) {
                ImageDecoder.createSource(context.getContentResolver(), uri);
            return null;
        final Drawable drawable =
        }
                ImageDecoder.decodeDrawable(source, LocalImageResolver::onHeaderDecoded);

        return drawable;
        int originalSize =
                (onlyBoundsOptions.outHeight > onlyBoundsOptions.outWidth)
                        ? onlyBoundsOptions.outHeight
                        : onlyBoundsOptions.outWidth;

        double ratio = (originalSize > MAX_SAFE_ICON_SIZE_PX)
                        ? (originalSize / MAX_SAFE_ICON_SIZE_PX)
                        : 1.0;

        BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
        bitmapOptions.inSampleSize = getPowerOfTwoForSampleRatio(ratio);
        InputStream input = context.getContentResolver().openInputStream(uri);
        Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
        input.close();
        return new BitmapDrawable(context.getResources(), bitmap);
    }
    }


    private static BitmapFactory.Options getBoundsOptionsForImage(Uri uri, Context context)
    public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight)
            throws IOException {
            throws IOException {
        BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
        final ImageDecoder.Source source =
        try (InputStream input = context.getContentResolver().openInputStream(uri)) {
                ImageDecoder.createSource(context.getContentResolver(), uri);
            if (input == null) {
        return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> {
                throw new IllegalArgumentException();
            final Size size = info.getSize();
            if (size.getWidth() > size.getHeight()) {
                if (size.getWidth() > maxWidth) {
                    final int targetHeight = size.getHeight() * maxWidth / size.getWidth();
                    decoder.setTargetSize(maxWidth, targetHeight);
                }
            } else {
                if (size.getHeight() > maxHeight) {
                    final int targetWidth = size.getWidth() * maxHeight / size.getHeight();
                    decoder.setTargetSize(targetWidth, maxHeight);
                }
                }
            onlyBoundsOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
        } catch (IllegalArgumentException iae) {
            onlyBoundsOptions.outWidth = -1;
            onlyBoundsOptions.outHeight = -1;
            Log.e(TAG, "error loading image", iae);
            }
            }
        return onlyBoundsOptions;
        });
    }
    }


    private static int getPowerOfTwoForSampleRatio(double ratio) {
    private static int getPowerOfTwoForSampleRatio(double ratio) {
        int k = Integer.highestOneBit((int) Math.floor(ratio));
        final int k = Integer.highestOneBit((int) Math.floor(ratio));
        return Math.max(1, k);
        return Math.max(1, k);
    }
    }

    private static void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
            ImageDecoder.Source source) {
        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
                : 1.0;
        decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio));
    }
}
}
+72 −0
Original line number Original line Diff line number Diff line
@@ -19,18 +19,25 @@ package com.android.systemui.statusbar.notification
import android.app.Notification
import android.app.Notification
import android.content.Context
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps
import android.graphics.drawable.AnimatedImageDrawable
import android.os.Handler
import android.os.Handler
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.NotificationListenerService.RankingMap
import android.service.notification.NotificationListenerService.RankingMap
import com.android.internal.statusbar.NotificationVisibility
import com.android.internal.statusbar.NotificationVisibility
import com.android.internal.widget.ConversationLayout
import com.android.internal.widget.ConversationLayout
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLayout
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationContentView
import com.android.systemui.statusbar.notification.row.NotificationContentView
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
import com.android.systemui.util.children
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Inject


@@ -57,6 +64,71 @@ class ConversationNotificationProcessor @Inject constructor(
    }
    }
}
}


/**
 * Tracks state related to animated images inside of notifications. Ex: starting and stopping
 * animations to conserve CPU and memory.
 */
@SysUISingleton
class AnimatedImageNotificationManager @Inject constructor(
    private val notificationEntryManager: NotificationEntryManager,
    private val headsUpManager: HeadsUpManager,
    private val statusBarStateController: StatusBarStateController
) {

    private var isStatusBarExpanded = false

    /** Begins listening to state changes and updating animations accordingly. */
    fun bind() {
        headsUpManager.addListener(object : OnHeadsUpChangedListener {
            override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
                entry.row?.let { row ->
                    updateAnimatedImageDrawables(row, animating = isHeadsUp || isStatusBarExpanded)
                }
            }
        })
        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
            override fun onExpandedChanged(isExpanded: Boolean) {
                isStatusBarExpanded = isExpanded
                notificationEntryManager.activeNotificationsForCurrentUser.forEach { entry ->
                    entry.row?.let { row ->
                        updateAnimatedImageDrawables(row, animating = isExpanded || row.isHeadsUp)
                    }
                }
            }
        })
        notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
            override fun onEntryInflated(entry: NotificationEntry) {
                entry.row?.let { row ->
                    updateAnimatedImageDrawables(
                            row,
                            animating = isStatusBarExpanded || row.isHeadsUp)
                }
            }
            override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry)
        })
    }

    private fun updateAnimatedImageDrawables(row: ExpandableNotificationRow, animating: Boolean) =
            (row.layouts?.asSequence() ?: emptySequence())
                    .flatMap { layout -> layout.allViews.asSequence() }
                    .flatMap { view ->
                        (view as? ConversationLayout)?.messagingGroups?.asSequence()
                                ?: (view as? MessagingLayout)?.messagingGroups?.asSequence()
                                ?: emptySequence()
                    }
                    .flatMap { messagingGroup -> messagingGroup.messageContainer.children }
                    .mapNotNull { view ->
                        (view as? MessagingImageMessage)
                                ?.let { imageMessage ->
                                    imageMessage.drawable as? AnimatedImageDrawable
                                }
                    }
                    .forEach { animatedImageDrawable ->
                        if (animating) animatedImageDrawable.start()
                        else animatedImageDrawable.stop()
                    }
}

/**
/**
 * Tracks state related to conversation notifications, and updates the UI of existing notifications
 * Tracks state related to conversation notifications, and updates the UI of existing notifications
 * when necessary.
 * when necessary.
+4 −1
Original line number Original line Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.NotificationClicker
import com.android.systemui.statusbar.notification.NotificationClicker
import com.android.systemui.statusbar.notification.NotificationEntryManager
import com.android.systemui.statusbar.notification.NotificationEntryManager
@@ -71,7 +72,8 @@ class NotificationsControllerImpl @Inject constructor(
    private val headsUpManager: HeadsUpManager,
    private val headsUpManager: HeadsUpManager,
    private val headsUpController: HeadsUpController,
    private val headsUpController: HeadsUpController,
    private val headsUpViewBinder: HeadsUpViewBinder,
    private val headsUpViewBinder: HeadsUpViewBinder,
    private val clickerBuilder: NotificationClicker.Builder
    private val clickerBuilder: NotificationClicker.Builder,
    private val animatedImageNotificationManager: AnimatedImageNotificationManager
) : NotificationsController {
) : NotificationsController {


    override fun initialize(
    override fun initialize(
@@ -100,6 +102,7 @@ class NotificationsControllerImpl @Inject constructor(
                bindRowCallback)
                bindRowCallback)
        headsUpViewBinder.setPresenter(presenter)
        headsUpViewBinder.setPresenter(presenter)
        notifBindPipelineInitializer.initialize()
        notifBindPipelineInitializer.initialize()
        animatedImageNotificationManager.bind()


        if (featureFlags.isNewNotifPipelineEnabled) {
        if (featureFlags.isNewNotifPipelineEnabled) {
            newNotifPipeline.get().initialize(
            newNotifPipeline.get().initialize(
+2 −16
Original line number Original line Diff line number Diff line
@@ -19,10 +19,7 @@ package com.android.systemui.statusbar.notification.row;
import android.app.ActivityManager;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.Notification;
import android.content.Context;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.net.Uri;
import android.os.Bundle;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.Parcelable;
@@ -81,7 +78,7 @@ public class NotificationInlineImageResolver implements ImageResolver {
     * @return True if has its internal cache, false otherwise.
     * @return True if has its internal cache, false otherwise.
     */
     */
    public boolean hasCache() {
    public boolean hasCache() {
        return mImageCache != null && !ActivityManager.isLowRamDeviceStatic();
        return mImageCache != null && !isLowRam();
    }
    }


    private boolean isLowRam() {
    private boolean isLowRam() {
@@ -110,11 +107,6 @@ public class NotificationInlineImageResolver implements ImageResolver {
                : R.dimen.notification_custom_view_max_image_height);
                : R.dimen.notification_custom_view_max_image_height);
    }
    }


    @VisibleForTesting
    protected BitmapDrawable resolveImageInternal(Uri uri) throws IOException {
        return (BitmapDrawable) LocalImageResolver.resolveImage(uri, mContext);
    }

    /**
    /**
     * To resolve image from specified uri directly. If the resulting image is larger than the
     * To resolve image from specified uri directly. If the resulting image is larger than the
     * maximum allowed size, scale it down.
     * maximum allowed size, scale it down.
@@ -123,13 +115,7 @@ public class NotificationInlineImageResolver implements ImageResolver {
     * @throws IOException Throws if failed at resolving the image.
     * @throws IOException Throws if failed at resolving the image.
     */
     */
    Drawable resolveImage(Uri uri) throws IOException {
    Drawable resolveImage(Uri uri) throws IOException {
        BitmapDrawable image = resolveImageInternal(uri);
        return LocalImageResolver.resolveImage(uri, mContext, mMaxImageWidth, mMaxImageHeight);
        if (image == null || image.getBitmap() == null) {
            throw new IOException("resolveImageInternal returned null for uri: " + uri);
        }
        Bitmap bitmap = image.getBitmap();
        image.setBitmap(Icon.scaleDownIfNecessary(bitmap, mMaxImageWidth, mMaxImageHeight));
        return image;
    }
    }


    @Override
    @Override
+0 −28
Original line number Original line Diff line number Diff line
@@ -69,32 +69,4 @@ public class NotificationInlineImageResolverTest extends SysuiTestCase {
        assertEquals("Height matches new config", mResolver.mMaxImageHeight, 20);
        assertEquals("Height matches new config", mResolver.mMaxImageHeight, 20);
        assertEquals("Width matches new config", mResolver.mMaxImageWidth, 15);
        assertEquals("Width matches new config", mResolver.mMaxImageWidth, 15);
    }
    }

    @Test
    public void resolveImage_sizeTooBig() throws IOException {
        doReturn(mBitmapDrawable).when(mResolver).resolveImageInternal(mUri);
        mResolver.mMaxImageHeight = 5;
        mResolver.mMaxImageWidth = 5;

        // original bitmap size is 10x10
        BitmapDrawable resolved = (BitmapDrawable) mResolver.resolveImage(mUri);
        Bitmap resolvedBitmap = resolved.getBitmap();
        assertEquals("Bitmap width reduced", 5, resolvedBitmap.getWidth());
        assertEquals("Bitmap height reduced", 5, resolvedBitmap.getHeight());
        assertNotSame("Bitmap replaced", resolvedBitmap, mBitmap);
    }

    @Test
    public void resolveImage_sizeOK() throws IOException {
        doReturn(mBitmapDrawable).when(mResolver).resolveImageInternal(mUri);
        mResolver.mMaxImageWidth = 15;
        mResolver.mMaxImageHeight = 15;

        // original bitmap size is 10x10
        BitmapDrawable resolved = (BitmapDrawable) mResolver.resolveImage(mUri);
        Bitmap resolvedBitmap = resolved.getBitmap();
        assertEquals("Bitmap width unchanged", 10, resolvedBitmap.getWidth());
        assertEquals("Bitmap height unchanged", 10, resolvedBitmap.getHeight());
        assertSame("Bitmap not replaced", resolvedBitmap, mBitmap);
    }
}
}