feat: in-chat snap preview

This commit is contained in:
rhunk
2023-11-01 16:45:31 +01:00
parent eb803df196
commit 94d58c4f46
6 changed files with 113 additions and 8 deletions

View File

@ -0,0 +1,88 @@
package me.rhunk.snapenhance.core.features.impl.ui
import android.annotation.SuppressLint
import android.graphics.*
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.Shape
import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
import me.rhunk.snapenhance.core.util.EvictingMap
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.media.PreviewUtils
import java.io.File
class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
private val mediaFileCache = mutableMapOf<String, File>() // mMediaId => mediaFile
private val bitmapCache = EvictingMap<String, Bitmap>(50) // filePath => bitmap
private val isEnabled get() = context.config.userInterface.snapPreview.get()
override fun init() {
if (!isEnabled) return
context.mappings.getMappedClass("callbacks", "ContentCallback").hook("handleContentResult", HookStage.BEFORE) { param ->
val contentResult = param.arg<Any>(0)
val classMethods = contentResult::class.java.methods
val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook
if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook
val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook
val mediaId = contentKey.getObjectField("mMediaId").toString()
mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString())
}
}
@SuppressLint("DiscouragedApi")
override fun onActivityCreate() {
if (!isEnabled) return
val chatMediaCardHeight = context.resources.getDimensionPixelSize(context.resources.getIdentifier("chat_media_card_height", "dimen", Constants.SNAPCHAT_PACKAGE_NAME))
val chatMediaCardSnapMargin = context.resources.getDimensionPixelSize(context.resources.getIdentifier("chat_media_card_snap_margin", "dimen", Constants.SNAPCHAT_PACKAGE_NAME))
val chatMediaCardSnapMarginStartSdl = context.resources.getDimensionPixelSize(context.resources.getIdentifier("chat_media_card_snap_margin_start_sdl", "dimen", Constants.SNAPCHAT_PACKAGE_NAME))
fun decodeMedia(file: File) = runCatching {
bitmapCache.getOrPut(file.absolutePath) {
PreviewUtils.resizeBitmap(
PreviewUtils.createPreviewFromFile(file) ?: return@runCatching null,
chatMediaCardHeight - chatMediaCardSnapMargin,
chatMediaCardHeight - chatMediaCardSnapMargin
)
}
}.getOrNull()
context.event.subscribe(BindViewEvent::class) { event ->
event.chatMessage { _, messageId ->
event.view.removeForegroundDrawable("snapPreview")
val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage
val messageReader = ProtoReader(message.messageContent ?: return@chatMessage)
val contentType = ContentType.fromMessageContainer(messageReader.followPath(4, 4))
if (contentType != ContentType.SNAP) return@chatMessage
val mediaIdKey = messageReader.getString(4, 5, 1, 3, 2, 2) ?: return@chatMessage
event.view.addForegroundDrawable("snapPreview", ShapeDrawable(object: Shape() {
override fun draw(canvas: Canvas, paint: Paint) {
if (canvas.height / context.resources.displayMetrics.density > 90) return
val bitmap = mediaFileCache[mediaIdKey]?.let { decodeMedia(it) } ?: return
canvas.drawBitmap(bitmap,
canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(),
(canvas.height - bitmap.height) / 2f,
null
)
}
}))
}
}
}
}

View File

@ -102,6 +102,7 @@ class FeatureManager(
HideFriendFeedEntry::class,
HideQuickAddFriendFeed::class,
CallStartConfirmation::class,
SnapPreview::class,
)
initializeFeatures()

View File

@ -8,6 +8,7 @@ import android.media.MediaDataSource
import android.media.MediaMetadataRetriever
import me.rhunk.snapenhance.common.data.FileType
import java.io.File
import kotlin.math.max
object PreviewUtils {
fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? {
@ -52,14 +53,20 @@ object PreviewUtils {
}
}
private fun resizeBitmap(bitmap: Bitmap, outWidth: Int, outHeight: Int): Bitmap? {
val scaleWidth = outWidth.toFloat() / bitmap.width
val scaleHeight = outHeight.toFloat() / bitmap.height
val matrix = Matrix()
matrix.postScale(scaleWidth, scaleHeight)
val resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
bitmap.recycle()
return resizedBitmap
fun resizeBitmap(source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val sourceWidth = source.getWidth()
val sourceHeight = source.getHeight()
val scale = max(outWidth.toFloat() / sourceWidth, outHeight.toFloat() / sourceHeight)
val dx = (outWidth - (scale * sourceWidth)) / 2F
val dy = (outHeight - (scale * sourceHeight)) / 2F
val dest = Bitmap.createBitmap(outWidth, outHeight, source.getConfig())
val canvas = Canvas(dest)
canvas.drawBitmap(source, Matrix().apply {
postScale(scale, scale)
postTranslate(dx, dy)
}, null)
return dest
}
fun mergeBitmapOverlay(originalMedia: Bitmap, overlayLayer: Bitmap): Bitmap {