Merge "AnimatedImageDrawable in MessageStyle+ConversationStyle"
This commit is contained in:
committed by
Android (Google) Code Review
commit
a2be7ce315
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user