diff options
5 files changed, 114 insertions, 89 deletions
diff --git a/core/java/com/android/internal/widget/LocalImageResolver.java b/core/java/com/android/internal/widget/LocalImageResolver.java index b4e108faee2d..3f205c785258 100644 --- a/core/java/com/android/internal/widget/LocalImageResolver.java +++ b/core/java/com/android/internal/widget/LocalImageResolver.java @@ -16,69 +16,61 @@ package com.android.internal.widget; -import android.annotation.Nullable; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; +import android.graphics.ImageDecoder; +import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.util.Log; +import android.util.Size; import java.io.IOException; -import java.io.InputStream; -/** - * A class to extract Bitmaps from a MessagingStyle message. - */ +/** 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; - @Nullable public static Drawable resolveImage(Uri uri, Context context) throws IOException { - BitmapFactory.Options onlyBoundsOptions = getBoundsOptionsForImage(uri, context); - if ((onlyBoundsOptions.outWidth == -1) || (onlyBoundsOptions.outHeight == -1)) { - return null; - } - - 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); + final ImageDecoder.Source source = + ImageDecoder.createSource(context.getContentResolver(), uri); + final Drawable drawable = + ImageDecoder.decodeDrawable(source, LocalImageResolver::onHeaderDecoded); + return drawable; } - private static BitmapFactory.Options getBoundsOptionsForImage(Uri uri, Context context) + public static Drawable resolveImage(Uri uri, Context context, int maxWidth, int maxHeight) throws IOException { - BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options(); - try (InputStream input = context.getContentResolver().openInputStream(uri)) { - if (input == null) { - throw new IllegalArgumentException(); + final ImageDecoder.Source source = + ImageDecoder.createSource(context.getContentResolver(), uri); + return ImageDecoder.decodeDrawable(source, (decoder, info, unused) -> { + 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) { - int k = Integer.highestOneBit((int) Math.floor(ratio)); + final int k = Integer.highestOneBit((int) Math.floor(ratio)); 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)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index 44b9bd26aa38..d1ab7ea55d57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -19,18 +19,25 @@ package com.android.systemui.statusbar.notification import android.app.Notification import android.content.Context import android.content.pm.LauncherApps +import android.graphics.drawable.AnimatedImageDrawable import android.os.Handler import android.service.notification.NotificationListenerService.Ranking import android.service.notification.NotificationListenerService.RankingMap import com.android.internal.statusbar.NotificationVisibility 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.qualifiers.Main +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationContentView 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 javax.inject.Inject @@ -58,6 +65,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 * when necessary. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 8f352ad55041..54ce4ede9770 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -22,6 +22,7 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.NotificationListener 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.NotificationClicker import com.android.systemui.statusbar.notification.NotificationEntryManager @@ -71,7 +72,8 @@ class NotificationsControllerImpl @Inject constructor( private val headsUpManager: HeadsUpManager, private val headsUpController: HeadsUpController, private val headsUpViewBinder: HeadsUpViewBinder, - private val clickerBuilder: NotificationClicker.Builder + private val clickerBuilder: NotificationClicker.Builder, + private val animatedImageNotificationManager: AnimatedImageNotificationManager ) : NotificationsController { override fun initialize( @@ -100,6 +102,7 @@ class NotificationsControllerImpl @Inject constructor( bindRowCallback) headsUpViewBinder.setPresenter(presenter) notifBindPipelineInitializer.initialize() + animatedImageNotificationManager.bind() if (featureFlags.isNewNotifPipelineEnabled) { newNotifPipeline.get().initialize( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java index 7bd192d850c1..44ccb68cce4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java @@ -19,10 +19,7 @@ package com.android.systemui.statusbar.notification.row; import android.app.ActivityManager; import android.app.Notification; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; @@ -81,7 +78,7 @@ public class NotificationInlineImageResolver implements ImageResolver { * @return True if has its internal cache, false otherwise. */ public boolean hasCache() { - return mImageCache != null && !ActivityManager.isLowRamDeviceStatic(); + return mImageCache != null && !isLowRam(); } private boolean isLowRam() { @@ -110,11 +107,6 @@ public class NotificationInlineImageResolver implements ImageResolver { : 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 * maximum allowed size, scale it down. @@ -123,13 +115,7 @@ public class NotificationInlineImageResolver implements ImageResolver { * @throws IOException Throws if failed at resolving the image. */ Drawable resolveImage(Uri uri) throws IOException { - BitmapDrawable image = resolveImageInternal(uri); - 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; + return LocalImageResolver.resolveImage(uri, mContext, mMaxImageWidth, mMaxImageHeight); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolverTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolverTest.java index 7f48cd1313fe..edf2b4c30ce4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolverTest.java @@ -69,32 +69,4 @@ public class NotificationInlineImageResolverTest extends SysuiTestCase { assertEquals("Height matches new config", mResolver.mMaxImageHeight, 20); 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); - } } |