diff --git a/changelog.d/5276.misc b/changelog.d/5276.misc new file mode 100644 index 0000000000..437bd28eb6 --- /dev/null +++ b/changelog.d/5276.misc @@ -0,0 +1 @@ +Improves bitmap memory usage by caching the shortcut images \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/AdaptiveIconTransformation.kt b/vector/src/main/java/im/vector/app/features/home/AdaptiveIconTransformation.kt new file mode 100644 index 0000000000..6eb41a1d85 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/AdaptiveIconTransformation.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * 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. + */ + +package im.vector.app.features.home + +import android.graphics.Bitmap +import android.graphics.Canvas +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.util.Util +import java.nio.ByteBuffer +import java.security.MessageDigest + +private const val ADAPTIVE_TRANSFORMATION_ID = "adaptive-icon-transform" +private val ID_BYTES = ADAPTIVE_TRANSFORMATION_ID.toByteArray() + +class AdaptiveIconTransformation(private val adaptiveIconSize: Int, private val adaptiveIconOuterSides: Float) : BitmapTransformation() { + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES) + messageDigest.update(ByteBuffer.allocate(8).putInt(adaptiveIconSize).putFloat(adaptiveIconOuterSides).array()) + } + + override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { + val insetBmp = Bitmap.createBitmap(adaptiveIconSize, adaptiveIconSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(insetBmp) + canvas.drawBitmap(toTransform, adaptiveIconOuterSides, adaptiveIconOuterSides, null) + canvas.setBitmap(null) + return insetBmp + } + + override fun equals(other: Any?): Boolean { + return if (other is AdaptiveIconTransformation) { + other.adaptiveIconSize == adaptiveIconSize && other.adaptiveIconOuterSides == adaptiveIconOuterSides + } else { + false + } + } + + override fun hashCode() = Util.hashCode(ADAPTIVE_TRANSFORMATION_ID.hashCode(), Util.hashCode(adaptiveIconSize, Util.hashCode(adaptiveIconOuterSides))) +} diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 2ee3233637..3678808b2d 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -26,12 +26,14 @@ import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.load.MultiTransformation import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.CircleCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target +import com.bumptech.glide.signature.ObjectKey import im.vector.app.core.contacts.MappedContact import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.glide.AvatarPlaceholder @@ -157,25 +159,45 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active fun shortcutDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { return glideRequests .asBitmap() - .let { - val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) - if (resolvedUrl != null) { - it.load(resolvedUrl) - } else { - val avatarColor = matrixItemColorProvider.getColor(matrixItem) - it.load(TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) - .toBitmap(width = iconSize, height = iconSize)) - } - } + .avatarOrText(matrixItem, iconSize) .apply(RequestOptions.centerCropTransform()) .submit(iconSize, iconSize) .get() } + @AnyThread + @Throws + fun adaptiveShortcutDrawable(glideRequests: GlideRequests, + matrixItem: MatrixItem, iconSize: Int, + adaptiveIconSize: Int, + adaptiveIconOuterSides: Float): Bitmap { + return glideRequests + .asBitmap() + .avatarOrText(matrixItem, iconSize) + .transform(CenterCrop(), AdaptiveIconTransformation(adaptiveIconSize, adaptiveIconOuterSides)) + .signature(ObjectKey("adaptive-icon")) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .submit(iconSize, iconSize) + .get() + } + + private fun GlideRequest.avatarOrText(matrixItem: MatrixItem, iconSize: Int): GlideRequest { + return this.let { + val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) + if (resolvedUrl != null) { + it.load(resolvedUrl) + } else { + val avatarColor = matrixItemColorProvider.getColor(matrixItem) + it.load(TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) + .toBitmap(width = iconSize, height = iconSize)) + } + } + } + @UiThread fun renderBlur(matrixItem: MatrixItem, imageView: ImageView, diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt index ee7edc021d..082d318cc7 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home import android.content.Context import android.content.pm.ShortcutInfo import android.graphics.Bitmap -import android.graphics.Canvas import android.os.Build import androidx.annotation.WorkerThread import androidx.core.content.pm.ShortcutInfoCompat @@ -61,7 +60,12 @@ class ShortcutCreator @Inject constructor( fun create(roomSummary: RoomSummary, rank: Int = 1): ShortcutInfoCompat { val intent = RoomDetailActivity.shortcutIntent(context, roomSummary.roomId) val bitmap = try { - avatarRenderer.shortcutDrawable(GlideApp.with(context), roomSummary.toMatrixItem(), iconSize) + val glideRequests = GlideApp.with(context) + val matrixItem = roomSummary.toMatrixItem() + when (useAdaptiveIcon) { + true -> avatarRenderer.adaptiveShortcutDrawable(glideRequests, matrixItem, iconSize, adaptiveIconSize, adaptiveIconOuterSides.toFloat()) + false -> avatarRenderer.shortcutDrawable(glideRequests, matrixItem, iconSize) + } } catch (failure: Throwable) { null } @@ -83,11 +87,7 @@ class ShortcutCreator @Inject constructor( private fun Bitmap.toProfileImageIcon(): IconCompat { return if (useAdaptiveIcon) { - val insetBmp = Bitmap.createBitmap(adaptiveIconSize, adaptiveIconSize, Bitmap.Config.ARGB_8888) - val canvas = Canvas(insetBmp) - canvas.drawBitmap(this, adaptiveIconOuterSides.toFloat(), adaptiveIconOuterSides.toFloat(), null) - - IconCompat.createWithAdaptiveBitmap(insetBmp) + IconCompat.createWithAdaptiveBitmap(this) } else { IconCompat.createWithBitmap(this) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/BitmapLoader.kt b/vector/src/main/java/im/vector/app/features/notifications/BitmapLoader.kt deleted file mode 100644 index d1c4624cdc..0000000000 --- a/vector/src/main/java/im/vector/app/features/notifications/BitmapLoader.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * 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. - */ - -package im.vector.app.features.notifications - -import android.content.Context -import android.graphics.Bitmap -import androidx.annotation.WorkerThread -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DecodeFormat -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BitmapLoader @Inject constructor(private val context: Context) { - - /** - * Avatar Url -> Bitmap - */ - private val cache = HashMap() - - /** - * Get icon of a room. - * If already in cache, use it, else load it and call BitmapLoaderListener.onBitmapsLoaded() when ready - */ - @WorkerThread - fun getRoomBitmap(path: String?): Bitmap? { - if (path == null) { - return null - } - - return cache.getOrPut(path) { - loadRoomBitmap(path) - } - } - - @WorkerThread - private fun loadRoomBitmap(path: String): Bitmap? { - return path.let { - try { - Glide.with(context) - .asBitmap() - .load(path) - .format(DecodeFormat.PREFER_ARGB_8888) - .submit() - .get() - } catch (e: Exception) { - Timber.e(e, "decodeFile failed") - null - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/notifications/IconLoader.kt b/vector/src/main/java/im/vector/app/features/notifications/IconLoader.kt deleted file mode 100644 index 3e68744c88..0000000000 --- a/vector/src/main/java/im/vector/app/features/notifications/IconLoader.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * 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. - */ - -package im.vector.app.features.notifications - -import android.content.Context -import android.os.Build -import androidx.annotation.WorkerThread -import androidx.core.graphics.drawable.IconCompat -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DecodeFormat -import com.bumptech.glide.request.RequestOptions -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class IconLoader @Inject constructor(private val context: Context) { - - /** - * Avatar Url -> IconCompat - */ - private val cache = HashMap() - - /** - * Get icon of a user. - * If already in cache, use it, else load it and call IconLoaderListener.onIconsLoaded() when ready - * Before Android P, this does nothing because the icon won't be used - */ - @WorkerThread - fun getUserIcon(path: String?): IconCompat? { - if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - return null - } - - return cache.getOrPut(path) { - loadUserIcon(path) - } - } - - @WorkerThread - private fun loadUserIcon(path: String): IconCompat? { - return path.let { - try { - Glide.with(context) - .asBitmap() - .load(path) - .apply(RequestOptions.circleCropTransform() - .format(DecodeFormat.PREFER_ARGB_8888)) - .submit() - .get() - } catch (e: Exception) { - Timber.e(e, "decodeFile failed") - null - }?.let { bitmap -> - IconCompat.createWithBitmap(bitmap) - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBitmapLoader.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBitmapLoader.kt new file mode 100644 index 0000000000..518b011ffd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBitmapLoader.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package im.vector.app.features.notifications + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.annotation.WorkerThread +import androidx.core.graphics.drawable.IconCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.resource.bitmap.CircleCrop +import com.bumptech.glide.signature.ObjectKey +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NotificationBitmapLoader @Inject constructor(private val context: Context) { + + /** + * Get icon of a room + */ + @WorkerThread + fun getRoomBitmap(path: String?): Bitmap? { + if (path == null) { + return null + } + return loadRoomBitmap(path) + } + + @WorkerThread + private fun loadRoomBitmap(path: String): Bitmap? { + return try { + Glide.with(context) + .asBitmap() + .load(path) + .format(DecodeFormat.PREFER_ARGB_8888) + .signature(ObjectKey("room-icon-notification")) + .submit() + .get() + } catch (e: Exception) { + Timber.e(e, "decodeFile failed") + null + } + } + + /** + * Get icon of a user. + * Before Android P, this does nothing because the icon won't be used + */ + @WorkerThread + fun getUserIcon(path: String?): IconCompat? { + if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return null + } + + return loadUserIcon(path) + } + + @WorkerThread + private fun loadUserIcon(path: String): IconCompat? { + return try { + val bitmap = Glide.with(context) + .asBitmap() + .load(path) + .transform(CircleCrop()) + .format(DecodeFormat.PREFER_ARGB_8888) + .signature(ObjectKey("user-icon-notification")) + .submit() + .get() + IconCompat.createWithBitmap(bitmap) + } catch (e: Exception) { + Timber.e(e, "decodeFile failed") + null + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index b57c81f686..8310c15daa 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -16,7 +16,6 @@ package im.vector.app.features.notifications -import android.content.Context import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person @@ -28,11 +27,9 @@ import timber.log.Timber import javax.inject.Inject class RoomGroupMessageCreator @Inject constructor( - private val iconLoader: IconLoader, - private val bitmapLoader: BitmapLoader, + private val bitmapLoader: NotificationBitmapLoader, private val stringProvider: StringProvider, - private val notificationUtils: NotificationUtils, - private val appContext: Context + private val notificationUtils: NotificationUtils ) { fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { @@ -41,7 +38,7 @@ class RoomGroupMessageCreator @Inject constructor( val roomIsGroup = !firstKnownRoomEvent.roomIsDirect val style = NotificationCompat.MessagingStyle(Person.Builder() .setName(userDisplayName) - .setIcon(iconLoader.getUserIcon(userAvatarUrl)) + .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) .setKey(firstKnownRoomEvent.matrixID) .build() ).also { @@ -92,7 +89,7 @@ class RoomGroupMessageCreator @Inject constructor( } else { Person.Builder() .setName(event.senderName) - .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) + .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath)) .setKey(event.senderId) .build() }