Merge pull request #5276 from vector-im/feature/adm/limit-inmemory-image-caches

Reducing bitmap memory footprint
This commit is contained in:
Benoit Marty 2022-02-22 13:58:58 +01:00 committed by GitHub
commit 211e1c2ce9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 194 additions and 168 deletions

1
changelog.d/5276.misc Normal file
View file

@ -0,0 +1 @@
Improves bitmap memory usage by caching the shortcut images

View file

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

View file

@ -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,7 +159,30 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
fun shortcutDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
return glideRequests
.asBitmap()
.let {
.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<Bitmap>.avatarOrText(matrixItem: MatrixItem, iconSize: Int): GlideRequest<Bitmap> {
return this.let {
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
if (resolvedUrl != null) {
it.load(resolvedUrl)
@ -171,9 +196,6 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.toBitmap(width = iconSize, height = iconSize))
}
}
.apply(RequestOptions.centerCropTransform())
.submit(iconSize, iconSize)
.get()
}
@UiThread

View file

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

View file

@ -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<String, Bitmap?>()
/**
* 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
}
}
}
}

View file

@ -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<String, IconCompat?>()
/**
* 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)
}
}
}
}

View file

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

View file

@ -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<NotifiableMessageEvent>, 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()
}