mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #5276 from vector-im/feature/adm/limit-inmemory-image-caches
Reducing bitmap memory footprint
This commit is contained in:
commit
211e1c2ce9
8 changed files with 194 additions and 168 deletions
1
changelog.d/5276.misc
Normal file
1
changelog.d/5276.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Improves bitmap memory usage by caching the shortcut images
|
|
@ -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)))
|
||||
}
|
|
@ -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<Bitmap>.avatarOrText(matrixItem: MatrixItem, iconSize: Int): GlideRequest<Bitmap> {
|
||||
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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue