Merge "AnimatedImageDrawable in MessageStyle+ConversationStyle"

This commit is contained in:
TreeHugger Robot
2020-11-30 19:43:40 +00:00
committed by Android (Google) Code Review
5 changed files with 114 additions and 89 deletions

View File

@@ -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));
}
}

View File

@@ -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
@@ -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
* when necessary.

View File

@@ -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(

View File

@@ -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

View File

@@ -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);
}
}