commit 3d50054d3841f2c8e2ec313e4906eb55aa5bed1e
parent 8e8220a55ea251752364d8e0add8c29f27c12e9b
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Fri, 3 Jan 2025 00:22:45 +0100
fix(core): remote media resolver
Use of coroutines to cancel network requests if necessary
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
Diffstat:
6 files changed, 57 insertions(+), 52 deletions(-)
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/snap/RemoteMediaResolver.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/snap/RemoteMediaResolver.kt
@@ -1,7 +1,7 @@
package me.rhunk.snapenhance.common.util.snap
import me.rhunk.snapenhance.common.Constants
-import me.rhunk.snapenhance.common.logger.AbstractLogger
+import me.rhunk.snapenhance.common.util.ktx.await
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -41,21 +41,21 @@ object RemoteMediaResolver {
.addHeader("User-Agent", Constants.USER_AGENT)
.build()
- /**
- * Download bolt media with memory allocation
- */
- fun downloadBoltMedia(protoKey: ByteArray, decryptionCallback: (InputStream) -> InputStream = { it }): ByteArray? {
- okHttpClient.newCall(newResolveRequest(protoKey)).execute().use { response ->
+ suspend inline fun downloadMedia(url: String, decryptionCallback: (InputStream) -> InputStream = { it }, result: (InputStream, Long) -> Unit) {
+ okHttpClient.newCall(Request.Builder().url(url).build()).await().use { response ->
if (!response.isSuccessful) {
- AbstractLogger.directDebug("Unexpected code $response")
- return null
+ throw Throwable("invalid response ${response.code}")
}
- return decryptionCallback(response.body.byteStream()).readBytes()
+ result(decryptionCallback(response.body.byteStream()), response.body.contentLength())
}
}
-
- inline fun downloadBoltMedia(protoKey: ByteArray, decryptionCallback: (InputStream) -> InputStream = { it }, resultCallback: (stream: InputStream, length: Long) -> Unit) {
- okHttpClient.newCall(newResolveRequest(protoKey)).execute().use { response ->
+
+ suspend inline fun downloadBoltMedia(
+ protoKey: ByteArray,
+ decryptionCallback: (InputStream) -> InputStream = { it },
+ resultCallback: (stream: InputStream, length: Long) -> Unit
+ ) {
+ okHttpClient.newCall(newResolveRequest(protoKey)).await().use { response ->
if (!response.isSuccessful) {
throw Throwable("invalid response ${response.code}")
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt
@@ -45,6 +45,7 @@ import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
import me.rhunk.snapenhance.common.util.ktx.copyToClipboard
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
+import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver
import me.rhunk.snapenhance.core.action.AbstractAction
import me.rhunk.snapenhance.core.features.impl.experiments.AddFriendSourceSpoof
import me.rhunk.snapenhance.core.features.impl.experiments.BetterLocation
@@ -405,13 +406,13 @@ class BulkMessagingAction : AbstractAction() {
if (bitmojiBitmap != null || friendInfo.bitmojiAvatarId == null || friendInfo.bitmojiSelfieId == null) return@withContext
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo.bitmojiSelfieId, friendInfo.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D) ?: return@withContext
+
runCatching {
- URL(bitmojiUrl).openStream().use { input ->
- bitmojiCache[friendInfo.bitmojiAvatarId ?: return@withContext] = BitmapFactory.decodeStream(input)
+ RemoteMediaResolver.downloadMedia(bitmojiUrl) { inputStream, length ->
+ bitmojiCache[friendInfo.bitmojiAvatarId ?: return@withContext] = BitmapFactory.decodeStream(inputStream).also {
+ bitmojiBitmap = it
+ }
}
- bitmojiBitmap = bitmojiCache[friendInfo.bitmojiAvatarId ?: return@withContext]
- }.onFailure {
- context.log.error("Failed to load bitmoji", it)
}
}
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt
@@ -31,7 +31,7 @@ data class DecodedAttachment(
}
@OptIn(ExperimentalEncodingApi::class)
- inline fun openStream(callback: (mediaStream: InputStream?, length: Long) -> Unit) {
+ suspend inline fun openStream(callback: (mediaStream: InputStream?, length: Long) -> Unit) {
boltKey?.let { mediaUrlKey ->
RemoteMediaResolver.downloadBoltMedia(Base64.UrlSafe.decode(mediaUrlKey), decryptionCallback = {
attachmentInfo?.encryption?.decryptInputStream(it) ?: it
@@ -39,11 +39,10 @@ data class DecodedAttachment(
callback(inputStream, length)
})
} ?: directUrl?.let { rawMediaUrl ->
- val connection = URL(rawMediaUrl).openConnection()
- connection.getInputStream().let {
+ RemoteMediaResolver.downloadMedia(rawMediaUrl, decryptionCallback = {
attachmentInfo?.encryption?.decryptInputStream(it) ?: it
- }.use {
- callback(it, connection.contentLengthLong)
+ }) { inputStream, length ->
+ callback(inputStream, length)
}
} ?: callback(null, 0)
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt
@@ -310,7 +310,7 @@ class Notifications : Feature("Notifications") {
}.send()
}
- private fun onMessageReceived(data: NotificationData, notificationType: String, message: Message) {
+ private suspend fun onMessageReceived(data: NotificationData, notificationType: String, message: Message) {
val conversationId = message.messageDescriptor?.conversationId.toString()
val orderKey = message.orderKey ?: return
val senderUsername by lazy {
@@ -487,16 +487,18 @@ class Notifications : Feature("Notifications") {
suspendCoroutine { continuation ->
conversationManager.fetchMessageByServerId(conversationId, serverMessageId.toLong(), onSuccess = {
if (it.senderId.toString() == context.database.myUserId) {
- param.invokeOriginal()
continuation.resumeWith(Result.success(Unit))
+ param.invokeOriginal()
return@fetchMessageByServerId
}
- onMessageReceived(notificationData, notificationType, it)
- continuation.resumeWith(Result.success(Unit))
+ context.coroutineScope.launch(coroutineDispatcher) {
+ continuation.resumeWith(Result.success(Unit))
+ onMessageReceived(notificationData, notificationType, it)
+ }
}, onError = {
context.log.error("Failed to fetch message id ${serverMessageId}: $it")
- param.invokeOriginal()
continuation.resumeWith(Result.success(Unit))
+ param.invokeOriginal()
})
}
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/FriendTracker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/FriendTracker.kt
@@ -318,7 +318,7 @@ class FriendTracker : Feature("Friend Tracker") {
// allow events when a notification is received
hookConstructor(HookStage.AFTER) { param ->
methods.first { it.name == "appStateChanged" }.let { method ->
- method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants.first { it.toString() == "ACTIVE" })
+ method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants!!.first { it.toString() == "ACTIVE" })
}
}
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt
@@ -3,6 +3,7 @@ package me.rhunk.snapenhance.core.messaging
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.google.gson.stream.JsonWriter
+import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.common.BuildConfig
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry
@@ -132,33 +133,35 @@ class ConversationExporter(
for (i in 0..5) {
printLog("downloading ${attachment.boltKey ?: attachment.directUrl}... (attempt ${i + 1}/5)")
runCatching {
- attachment.openStream { downloadedInputStream, _ ->
- MediaDownloaderHelper.getSplitElements(downloadedInputStream!!) { type, splitInputStream ->
- val mediaKey = "${type}_${attachment.mediaUniqueId}"
- val bufferedInputStream = BufferedInputStream(splitInputStream)
- val fileType = MediaDownloaderHelper.getFileType(bufferedInputStream)
- val mediaFile = cacheFolder.resolve("$mediaKey.${fileType.fileExtension}")
-
- mediaFile.outputStream().use { fos ->
- bufferedInputStream.copyTo(fos)
- }
+ runBlocking {
+ attachment.openStream { downloadedInputStream, _ ->
+ MediaDownloaderHelper.getSplitElements(downloadedInputStream!!) { type, splitInputStream ->
+ val mediaKey = "${type}_${attachment.mediaUniqueId}"
+ val bufferedInputStream = BufferedInputStream(splitInputStream)
+ val fileType = MediaDownloaderHelper.getFileType(bufferedInputStream)
+ val mediaFile = cacheFolder.resolve("$mediaKey.${fileType.fileExtension}")
+
+ mediaFile.outputStream().use { fos ->
+ bufferedInputStream.copyTo(fos)
+ }
- writeThreadExecutor.execute {
- outputFileStream.write("<div class=\"media-$mediaKey\"><!-- ".toByteArray())
- mediaFile.inputStream().use {
- val deflateInputStream = DeflaterInputStream(it, Deflater(Deflater.BEST_SPEED, true))
- (newBase64InputStream.newInstance(
- deflateInputStream,
- android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
- true
- ) as InputStream).copyTo(outputFileStream)
- outputFileStream.write(" --></div>\n".toByteArray())
- outputFileStream.flush()
+ writeThreadExecutor.execute {
+ outputFileStream.write("<div class=\"media-$mediaKey\"><!-- ".toByteArray())
+ mediaFile.inputStream().use {
+ val deflateInputStream = DeflaterInputStream(it, Deflater(Deflater.BEST_SPEED, true))
+ (newBase64InputStream.newInstance(
+ deflateInputStream,
+ android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
+ true
+ ) as InputStream).copyTo(outputFileStream)
+ outputFileStream.write(" --></div>\n".toByteArray())
+ outputFileStream.flush()
+ }
}
}
- }
- writeThreadExecutor.execute {
- downloadedMediaIdCache.add(attachment.mediaUniqueId!!)
+ writeThreadExecutor.execute {
+ downloadedMediaIdCache.add(attachment.mediaUniqueId!!)
+ }
}
}
return@decode