commit 7b5a411a5d9756da1f3ef7e7e8478253126ed88c
parent 249569468ce871173a3f60c53a7aadc7f89e3cf8
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat,  9 Sep 2023 15:35:26 +0200

refactor: core package

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt | 1+
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt | 6+++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 8++++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt | 5-----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/ActivityLauncherHelper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt | 6+++---
Dcore/src/main/kotlin/me/rhunk/snapenhance/Logger.kt | 124-------------------------------------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt | 8++++----
Acore/src/main/kotlin/me/rhunk/snapenhance/core/Logger.kt | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/LocaleWrapper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MappingsWrapper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MessageLoggerWrapper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/ConversationMessage.kt | 10+++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendFeedEntry.kt | 8++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendInfo.kt | 8++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/StoryEntry.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/UserConversationLink.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt | 2+-
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/ReflectionHelper.kt | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/SQLiteDatabaseHelper.kt | 32++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/SerializableDataObject.kt | 23+++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/download/HttpServer.kt | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/download/RemoteMediaResolver.kt | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/export/MessageExporter.kt | 343+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidCompatExtensions.kt | 13+++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/DbCursorExt.kt | 38++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/XposedHelperExt.kt | 27+++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoEditor.kt | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoReader.kt | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoWriter.kt | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/WireType.kt | 14++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/BitmojiSelfie.kt | 21+++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/EncryptionHelper.kt | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/MediaDownloaderHelper.kt | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/PreviewUtils.kt | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/SnapWidgetBroadcastReceiverHelper.kt | 25+++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 14+++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/ProfilePictureDownloader.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt | 6+++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableReplayInFF.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt | 16++++++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/OldBitmojiSelfie.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SendOverride.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt | 8++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt | 2+-
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt | 94-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt | 120-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt | 32--------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/SerializableDataObject.kt | 23-----------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt | 140-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt | 53-----------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt | 343-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/ktx/AndroidCompatExtensions.kt | 13-------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/ktx/DbCursorExt.kt | 38--------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/ktx/XposedHelperExt.kt | 27---------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt | 68--------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt | 176-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt | 118-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/WireType.kt | 14--------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt | 21---------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt | 49-------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt | 71-----------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt | 87-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapWidgetBroadcastReceiverHelper.kt | 25-------------------------
97 files changed, 1749 insertions(+), 1747 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance import android.content.SharedPreferences import android.util.Log import com.google.gson.GsonBuilder +import me.rhunk.snapenhance.core.LogLevel import java.io.File import java.io.OutputStream import java.io.RandomAccessFile diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.bridge import android.app.Service import android.content.Intent import android.os.IBinder -import me.rhunk.snapenhance.LogLevel +import me.rhunk.snapenhance.core.LogLevel import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.core.bridge.types.BridgeFileType @@ -14,7 +14,7 @@ import me.rhunk.snapenhance.core.database.objects.FriendInfo import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.download.DownloadProcessor -import me.rhunk.snapenhance.util.SerializableDataObject +import me.rhunk.snapenhance.core.util.SerializableDataObject import kotlin.system.measureTimeMillis class BridgeService : Service() { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -26,7 +26,7 @@ import me.rhunk.snapenhance.core.download.data.DownloadRequest import me.rhunk.snapenhance.core.download.data.DownloadStage import me.rhunk.snapenhance.core.download.data.InputMedia import me.rhunk.snapenhance.core.download.data.MediaEncryptionKeyPair -import me.rhunk.snapenhance.util.download.RemoteMediaResolver +import me.rhunk.snapenhance.core.util.download.RemoteMediaResolver import java.io.File import java.io.InputStream import java.net.HttpURLConnection diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt @@ -6,9 +6,9 @@ import android.database.sqlite.SQLiteDatabase import me.rhunk.snapenhance.core.download.data.DownloadMetadata import me.rhunk.snapenhance.core.download.data.DownloadStage import me.rhunk.snapenhance.core.download.data.MediaDownloadSource -import me.rhunk.snapenhance.util.SQLiteDatabaseHelper -import me.rhunk.snapenhance.util.ktx.getIntOrNull -import me.rhunk.snapenhance.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper +import me.rhunk.snapenhance.core.util.ktx.getIntOrNull +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull class DownloadTaskManager { private lateinit var taskDatabase: SQLiteDatabase diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt @@ -4,9 +4,9 @@ import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegSession import com.arthenica.ffmpegkit.Level import kotlinx.coroutines.suspendCancellableCoroutine -import me.rhunk.snapenhance.LogLevel +import me.rhunk.snapenhance.core.LogLevel import me.rhunk.snapenhance.LogManager -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.config.impl.DownloaderConfig import java.io.File import java.util.concurrent.Executors diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -7,10 +7,10 @@ import me.rhunk.snapenhance.core.messaging.FriendStreaks import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.core.messaging.MessagingRuleType -import me.rhunk.snapenhance.util.SQLiteDatabaseHelper -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getLongOrNull -import me.rhunk.snapenhance.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper +import me.rhunk.snapenhance.core.util.ktx.getInteger +import me.rhunk.snapenhance.core.util.ktx.getLongOrNull +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull import java.util.concurrent.Executors diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt @@ -15,7 +15,7 @@ import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.bridge.ForceStartActivity import me.rhunk.snapenhance.ui.util.ImageRequestHelper -import me.rhunk.snapenhance.util.snap.BitmojiSelfie +import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie class StreaksReminder( private val remoteSideContext: RemoteSideContext? = null diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt @@ -53,8 +53,8 @@ import androidx.compose.ui.window.Dialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.LogChannels -import me.rhunk.snapenhance.LogLevel +import me.rhunk.snapenhance.core.LogChannels +import me.rhunk.snapenhance.core.LogLevel import me.rhunk.snapenhance.LogReader import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.action.EnumAction diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt @@ -47,7 +47,7 @@ import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo -import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper +import me.rhunk.snapenhance.core.util.snap.SnapWidgetBroadcastReceiverHelper class AddFriendDialog( private val context: RemoteSideContext, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -31,7 +31,7 @@ import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.core.messaging.SocialScope import me.rhunk.snapenhance.ui.util.BitmojiImage -import me.rhunk.snapenhance.util.snap.BitmojiSelfie +import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie class ScopeContent( private val context: RemoteSideContext, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -57,7 +57,7 @@ import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.AlertDialogs import me.rhunk.snapenhance.ui.util.BitmojiImage import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset -import me.rhunk.snapenhance.util.snap.BitmojiSelfie +import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie class SocialSection : Section() { private lateinit var friendList: List<MessagingFriendInfo> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt @@ -1,14 +1,10 @@ package me.rhunk.snapenhance.ui.setup.screens.impl -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -21,7 +17,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.util.AlertDialogs diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ActivityLauncherHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ActivityLauncherHelper.kt @@ -4,7 +4,7 @@ import android.content.Intent import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger typealias ActivityLauncherCallback = (resultCode: Int, intent: Intent?) -> Unit diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt @@ -9,15 +9,15 @@ import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.eventbus.events.impl.OnSnapInteractionEvent import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.core.util.snap.SnapWidgetBroadcastReceiverHelper import me.rhunk.snapenhance.data.wrapper.impl.MessageContent import me.rhunk.snapenhance.data.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.manager.Manager -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.ktx.setObjectField -import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper class EventDispatcher( private val context: ModContext diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt @@ -1,123 +0,0 @@ -package me.rhunk.snapenhance - -import android.annotation.SuppressLint -import android.util.Log -import de.robv.android.xposed.XposedBridge -import me.rhunk.snapenhance.core.bridge.BridgeClient -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.hook - -enum class LogLevel( - val letter: String, - val shortName: String, - val priority: Int = Log.INFO -) { - VERBOSE("V", "verbose", Log.VERBOSE), - DEBUG("D", "debug", Log.DEBUG), - INFO("I", "info", Log.INFO), - WARN("W", "warn", Log.WARN), - ERROR("E", "error", Log.ERROR), - ASSERT("A", "assert", Log.ASSERT); - - companion object { - fun fromLetter(letter: String): LogLevel? { - return values().find { it.letter == letter } - } - - fun fromShortName(shortName: String): LogLevel? { - return values().find { it.shortName == shortName } - } - - fun fromPriority(priority: Int): LogLevel? { - return values().find { it.priority == priority } - } - } -} - -enum class LogChannels(val channel: String, val shortName: String) { - CORE("SnapEnhanceCore", "core"), - NATIVE("SnapEnhanceNative", "native"), - MANAGER("SnapEnhanceManager", "manager"), - XPOSED("LSPosed-Bridge", "xposed"); - - companion object { - fun fromChannel(channel: String): LogChannels? { - return values().find { it.channel == channel } - } - } -} - - -@SuppressLint("PrivateApi") -class Logger( - private val bridgeClient: BridgeClient -) { - companion object { - private const val TAG = "SnapEnhanceCore" - - fun directDebug(message: Any?, tag: String = TAG) { - Log.println(Log.DEBUG, tag, message.toString()) - } - - fun directError(message: Any?, throwable: Throwable, tag: String = TAG) { - Log.println(Log.ERROR, tag, message.toString()) - Log.println(Log.ERROR, tag, throwable.toString()) - } - - fun xposedLog(message: Any?, tag: String = TAG) { - Log.println(Log.INFO, tag, message.toString()) - XposedBridge.log("$tag: $message") - } - - fun xposedLog(message: Any?, throwable: Throwable, tag: String = TAG) { - Log.println(Log.INFO, tag, message.toString()) - XposedBridge.log("$tag: $message") - XposedBridge.log(throwable) - } - } - - private var invokeOriginalPrintLog: (Int, String, String) -> Unit - - init { - val printLnMethod = Log::class.java.getDeclaredMethod("println", Int::class.java, String::class.java, String::class.java) - printLnMethod.hook(HookStage.BEFORE) { param -> - val priority = param.arg(0) as Int - val tag = param.arg(1) as String - val message = param.arg(2) as String - internalLog(tag, LogLevel.fromPriority(priority) ?: LogLevel.INFO, message) - } - - invokeOriginalPrintLog = { priority, tag, message -> - XposedBridge.invokeOriginalMethod( - printLnMethod, - null, - arrayOf(priority, tag, message) - ) - } - } - - private fun internalLog(tag: String, logLevel: LogLevel, message: Any?) { - runCatching { - bridgeClient.broadcastLog(tag, logLevel.shortName, message.toString()) - }.onFailure { - invokeOriginalPrintLog(logLevel.priority, tag, message.toString()) - } - } - - fun debug(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.DEBUG, message) - - fun error(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ERROR, message) - - fun error(message: Any?, throwable: Throwable, tag: String = TAG) { - internalLog(tag, LogLevel.ERROR, message) - internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString()) - } - - fun info(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.INFO, message) - - fun verbose(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.VERBOSE, message) - - fun warn(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.WARN, message) - - fun assert(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ASSERT, message) -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -11,19 +11,20 @@ import android.widget.Toast import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.asCoroutineDispatcher +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.bridge.wrapper.MappingsWrapper import me.rhunk.snapenhance.core.config.ModConfig import me.rhunk.snapenhance.core.database.DatabaseAccess import me.rhunk.snapenhance.core.eventbus.EventBus +import me.rhunk.snapenhance.core.util.download.HttpServer import me.rhunk.snapenhance.data.MessageSender import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.manager.impl.ActionManager import me.rhunk.snapenhance.manager.impl.FeatureManager import me.rhunk.snapenhance.nativelib.NativeConfig import me.rhunk.snapenhance.nativelib.NativeLib -import me.rhunk.snapenhance.util.download.HttpServer import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import kotlin.reflect.KClass diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -8,16 +8,17 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import me.rhunk.snapenhance.bridge.SyncCallback import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.eventbus.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo +import me.rhunk.snapenhance.core.util.ktx.getApplicationInfoCompat import me.rhunk.snapenhance.data.SnapClassCache import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.ktx.getApplicationInfoCompat import kotlin.time.ExperimentalTime import kotlin.time.measureTime diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt @@ -11,17 +11,17 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.action.AbstractAction +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry +import me.rhunk.snapenhance.core.util.CallbackBuilder +import me.rhunk.snapenhance.core.util.export.ExportFormat +import me.rhunk.snapenhance.core.util.export.MessageExporter import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import me.rhunk.snapenhance.util.CallbackBuilder -import me.rhunk.snapenhance.util.export.ExportFormat -import me.rhunk.snapenhance.util.export.MessageExporter import java.io.File import kotlin.math.absoluteValue diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/Logger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/Logger.kt @@ -0,0 +1,123 @@ +package me.rhunk.snapenhance.core + +import android.annotation.SuppressLint +import android.util.Log +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.core.bridge.BridgeClient +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.hook + +enum class LogLevel( + val letter: String, + val shortName: String, + val priority: Int = Log.INFO +) { + VERBOSE("V", "verbose", Log.VERBOSE), + DEBUG("D", "debug", Log.DEBUG), + INFO("I", "info", Log.INFO), + WARN("W", "warn", Log.WARN), + ERROR("E", "error", Log.ERROR), + ASSERT("A", "assert", Log.ASSERT); + + companion object { + fun fromLetter(letter: String): LogLevel? { + return values().find { it.letter == letter } + } + + fun fromShortName(shortName: String): LogLevel? { + return values().find { it.shortName == shortName } + } + + fun fromPriority(priority: Int): LogLevel? { + return values().find { it.priority == priority } + } + } +} + +enum class LogChannels(val channel: String, val shortName: String) { + CORE("SnapEnhanceCore", "core"), + NATIVE("SnapEnhanceNative", "native"), + MANAGER("SnapEnhanceManager", "manager"), + XPOSED("LSPosed-Bridge", "xposed"); + + companion object { + fun fromChannel(channel: String): LogChannels? { + return values().find { it.channel == channel } + } + } +} + + +@SuppressLint("PrivateApi") +class Logger( + private val bridgeClient: BridgeClient +) { + companion object { + private const val TAG = "SnapEnhanceCore" + + fun directDebug(message: Any?, tag: String = TAG) { + Log.println(Log.DEBUG, tag, message.toString()) + } + + fun directError(message: Any?, throwable: Throwable, tag: String = TAG) { + Log.println(Log.ERROR, tag, message.toString()) + Log.println(Log.ERROR, tag, throwable.toString()) + } + + fun xposedLog(message: Any?, tag: String = TAG) { + Log.println(Log.INFO, tag, message.toString()) + XposedBridge.log("$tag: $message") + } + + fun xposedLog(message: Any?, throwable: Throwable, tag: String = TAG) { + Log.println(Log.INFO, tag, message.toString()) + XposedBridge.log("$tag: $message") + XposedBridge.log(throwable) + } + } + + private var invokeOriginalPrintLog: (Int, String, String) -> Unit + + init { + val printLnMethod = Log::class.java.getDeclaredMethod("println", Int::class.java, String::class.java, String::class.java) + printLnMethod.hook(HookStage.BEFORE) { param -> + val priority = param.arg(0) as Int + val tag = param.arg(1) as String + val message = param.arg(2) as String + internalLog(tag, LogLevel.fromPriority(priority) ?: LogLevel.INFO, message) + } + + invokeOriginalPrintLog = { priority, tag, message -> + XposedBridge.invokeOriginalMethod( + printLnMethod, + null, + arrayOf(priority, tag, message) + ) + } + } + + private fun internalLog(tag: String, logLevel: LogLevel, message: Any?) { + runCatching { + bridgeClient.broadcastLog(tag, logLevel.shortName, message.toString()) + }.onFailure { + invokeOriginalPrintLog(logLevel.priority, tag, message.toString()) + } + } + + fun debug(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.DEBUG, message) + + fun error(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ERROR, message) + + fun error(message: Any?, throwable: Throwable, tag: String = TAG) { + internalLog(tag, LogLevel.ERROR, message) + internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString()) + } + + fun info(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.INFO, message) + + fun verbose(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.VERBOSE, message) + + fun warn(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.WARN, message) + + fun assert(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ASSERT, message) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/LocaleWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/LocaleWrapper.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.core.bridge.wrapper import android.content.Context import com.google.gson.JsonObject import com.google.gson.JsonParser -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.data.LocalePair import java.util.Locale diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MappingsWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MappingsWrapper.kt @@ -5,7 +5,7 @@ import com.google.gson.GsonBuilder import com.google.gson.JsonElement import com.google.gson.JsonParser import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.bridge.FileLoaderWrapper import me.rhunk.snapenhance.core.bridge.types.BridgeFileType import me.rhunk.snapmapper.Mapper diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MessageLoggerWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MessageLoggerWrapper.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.bridge.wrapper import android.content.ContentValues import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.util.SQLiteDatabaseHelper +import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper import java.io.File class MessageLoggerWrapper( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -2,8 +2,8 @@ package me.rhunk.snapenhance.core.database import android.annotation.SuppressLint import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.database.objects.ConversationMessage import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry import me.rhunk.snapenhance.core.database.objects.FriendInfo diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/ConversationMessage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/ConversationMessage.kt @@ -4,12 +4,12 @@ import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.core.database.DatabaseObject +import me.rhunk.snapenhance.core.util.ktx.getBlobOrNull +import me.rhunk.snapenhance.core.util.ktx.getInteger +import me.rhunk.snapenhance.core.util.ktx.getLong +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.util.ktx.getBlobOrNull -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getLong -import me.rhunk.snapenhance.util.ktx.getStringOrNull -import me.rhunk.snapenhance.util.protobuf.ProtoReader @Suppress("ArrayInDataClass") data class ConversationMessage( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendFeedEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendFeedEntry.kt @@ -3,10 +3,10 @@ package me.rhunk.snapenhance.core.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.core.database.DatabaseObject -import me.rhunk.snapenhance.util.ktx.getIntOrNull -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getLong -import me.rhunk.snapenhance.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.ktx.getIntOrNull +import me.rhunk.snapenhance.core.util.ktx.getInteger +import me.rhunk.snapenhance.core.util.ktx.getLong +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull data class FriendFeedEntry( var id: Int = 0, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendInfo.kt @@ -3,10 +3,10 @@ package me.rhunk.snapenhance.core.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.core.database.DatabaseObject -import me.rhunk.snapenhance.util.SerializableDataObject -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getLong -import me.rhunk.snapenhance.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.SerializableDataObject +import me.rhunk.snapenhance.core.util.ktx.getInteger +import me.rhunk.snapenhance.core.util.ktx.getLong +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull data class FriendInfo( var id: Int = 0, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/StoryEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/StoryEntry.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.core.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.core.database.DatabaseObject -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.ktx.getInteger +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull data class StoryEntry( var id: Int = 0, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/UserConversationLink.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/UserConversationLink.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.core.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.core.database.DatabaseObject -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.util.ktx.getInteger +import me.rhunk.snapenhance.core.util.ktx.getStringOrNull class UserConversationLink( var userId: String? = null, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.core.messaging -import me.rhunk.snapenhance.util.SerializableDataObject +import me.rhunk.snapenhance.core.util.SerializableDataObject enum class RuleState( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt @@ -0,0 +1,93 @@ +package me.rhunk.snapenhance.core.util + +import de.robv.android.xposed.XC_MethodHook +import me.rhunk.snapenhance.hook.HookAdapter +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +class CallbackBuilder( + private val callbackClass: Class<*> +) { + internal class Override( + val methodName: String, + val shouldUnhook: Boolean = true, + val callback: (HookAdapter) -> Unit + ) + + private val methodOverrides = mutableListOf<Override>() + + fun override(methodName: String, shouldUnhook: Boolean = true, callback: (HookAdapter) -> Unit = {}): CallbackBuilder { + methodOverrides.add(Override(methodName, shouldUnhook, callback)) + return this + } + + fun build(): Any { + //get the first param of the first constructor to get the class of the invoker + val invokerClass: Class<*> = callbackClass.constructors[0].parameterTypes[0] + //get the invoker field based on the invoker class + val invokerField = callbackClass.fields.first { field: Field -> + field.type.isAssignableFrom(invokerClass) + } + //get the callback field based on the callback class + val callbackInstance = createEmptyObject(callbackClass.constructors[0])!! + val callbackInstanceHashCode: Int = callbackInstance.hashCode() + val callbackInstanceClass = callbackInstance.javaClass + + val unhooks = mutableListOf<XC_MethodHook.Unhook>() + + callbackInstanceClass.methods.forEach { method -> + if (method.declaringClass != callbackInstanceClass) return@forEach + if (Modifier.isPrivate(method.modifiers)) return@forEach + + //default hook that unhooks the callback and returns null + val defaultHook: (HookAdapter) -> Boolean = defaultHook@{ + //checking invokerField ensure that's the callback was created by the CallbackBuilder + if (invokerField.get(it.thisObject()) != null) return@defaultHook false + if ((it.thisObject() as Any).hashCode() != callbackInstanceHashCode) return@defaultHook false + it.setResult(null) + true + } + + var hook: (HookAdapter) -> Unit = { defaultHook(it) } + + //override the default hook if the method is in the override list + methodOverrides.find { it.methodName == method.name }?.run { + hook = { + if (defaultHook(it)) { + callback(it) + if (shouldUnhook) unhooks.forEach { unhook -> unhook.unhook() } + } + } + } + + unhooks.add(Hooker.hook(method, HookStage.BEFORE, hook)) + } + return callbackInstance + } + + companion object { + fun createEmptyObject(constructor: Constructor<*>): Any? { + //compute the args for the constructor with null or default primitive values + val args = constructor.parameterTypes.map { type: Class<*> -> + if (type.isPrimitive) { + when (type.name) { + "boolean" -> return@map false + "byte" -> return@map 0.toByte() + "char" -> return@map 0.toChar() + "short" -> return@map 0.toShort() + "int" -> return@map 0 + "long" -> return@map 0L + "float" -> return@map 0f + "double" -> return@map 0.0 + } + } + null + }.toTypedArray() + return constructor.newInstance(*args) + } + + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ReflectionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ReflectionHelper.kt @@ -0,0 +1,119 @@ +package me.rhunk.snapenhance.core.util + +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.Arrays +import java.util.Objects + +object ReflectionHelper { + /** + * Searches for a field with a class that has a method with the specified name + */ + fun searchFieldWithClassMethod(clazz: Class<*>, methodName: String): Field? { + return clazz.declaredFields.firstOrNull { f: Field? -> + try { + return@firstOrNull Arrays.stream( + f!!.type.declaredMethods + ).anyMatch { method: Method -> method.name == methodName } + } catch (e: Exception) { + return@firstOrNull false + } + } + } + + fun searchFieldByType(clazz: Class<*>, type: Class<*>): Field? { + return clazz.declaredFields.firstOrNull { f: Field? -> f!!.type == type } + } + + fun searchFieldTypeInSuperClasses(clazz: Class<*>, type: Class<*>): Field? { + val field = searchFieldByType(clazz, type) + if (field != null) { + return field + } + val superclass = clazz.superclass + return superclass?.let { searchFieldTypeInSuperClasses(it, type) } + } + + fun searchFieldStartsWithToString( + clazz: Class<*>, + instance: Any, + toString: String? + ): Field? { + return clazz.declaredFields.firstOrNull { f: Field -> + try { + f.isAccessible = true + return@firstOrNull Objects.requireNonNull(f[instance]).toString() + .startsWith( + toString!! + ) + } catch (e: Throwable) { + return@firstOrNull false + } + } + } + + + fun searchFieldContainsToString( + clazz: Class<*>, + instance: Any?, + toString: String? + ): Field? { + return clazz.declaredFields.firstOrNull { f: Field -> + try { + f.isAccessible = true + return@firstOrNull Objects.requireNonNull(f[instance]).toString() + .contains(toString!!) + } catch (e: Throwable) { + return@firstOrNull false + } + } + } + + fun searchFirstFieldTypeInClassRecursive(clazz: Class<*>, type: Class<*>): Field? { + return clazz.declaredFields.firstOrNull { + val field = searchFieldByType(it.type, type) + return@firstOrNull field != null + } + } + + /** + * Searches for a field with a class that has a method with the specified return type + */ + fun searchMethodWithReturnType(clazz: Class<*>, returnType: Class<*>): Method? { + return clazz.declaredMethods.first { m: Method -> m.returnType == returnType } + } + + /** + * Searches for a field with a class that has a method with the specified return type and parameter types + */ + fun searchMethodWithParameterAndReturnType( + aClass: Class<*>, + returnType: Class<*>, + vararg parameters: Class<*> + ): Method? { + return aClass.declaredMethods.firstOrNull { m: Method -> + if (m.returnType != returnType) { + return@firstOrNull false + } + val parameterTypes = m.parameterTypes + if (parameterTypes.size != parameters.size) { + return@firstOrNull false + } + for (i in parameterTypes.indices) { + if (parameterTypes[i] != parameters[i]) { + return@firstOrNull false + } + } + true + } + } + + fun getDeclaredFieldsRecursively(clazz: Class<*>): List<Field> { + val fields = clazz.declaredFields.toMutableList() + val superclass = clazz.superclass + if (superclass != null) { + fields.addAll(getDeclaredFieldsRecursively(superclass)) + } + return fields + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/SQLiteDatabaseHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/SQLiteDatabaseHelper.kt @@ -0,0 +1,31 @@ +package me.rhunk.snapenhance.core.util + +import android.annotation.SuppressLint +import android.database.sqlite.SQLiteDatabase +import me.rhunk.snapenhance.core.Logger + +object SQLiteDatabaseHelper { + @SuppressLint("Range") + fun createTablesFromSchema(sqLiteDatabase: SQLiteDatabase, databaseSchema: Map<String, List<String>>) { + databaseSchema.forEach { (tableName, columns) -> + sqLiteDatabase.execSQL("CREATE TABLE IF NOT EXISTS $tableName (${columns.joinToString(", ")})") + + val cursor = sqLiteDatabase.rawQuery("PRAGMA table_info($tableName)", null) + val existingColumns = mutableListOf<String>() + while (cursor.moveToNext()) { + existingColumns.add(cursor.getString(cursor.getColumnIndex("name")) + " " + cursor.getString(cursor.getColumnIndex("type"))) + } + cursor.close() + + val newColumns = columns.filter { + existingColumns.none { existingColumn -> it.startsWith(existingColumn) } + } + + if (newColumns.isEmpty()) return@forEach + + Logger.directDebug("Schema for table $tableName has changed") + sqLiteDatabase.execSQL("DROP TABLE $tableName") + sqLiteDatabase.execSQL("CREATE TABLE IF NOT EXISTS $tableName (${columns.joinToString(", ")})") + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/SerializableDataObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/SerializableDataObject.kt @@ -0,0 +1,22 @@ +package me.rhunk.snapenhance.core.util + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +open class SerializableDataObject { + companion object { + val gson: Gson = GsonBuilder().create() + + inline fun <reified T : SerializableDataObject> fromJson(json: String): T { + return gson.fromJson(json, T::class.java) + } + + inline fun <reified T : SerializableDataObject> fromJson(json: String, type: Class<T>): T { + return gson.fromJson(json, type) + } + } + + fun toJson(): String { + return gson.toJson(this) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/download/HttpServer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/download/HttpServer.kt @@ -0,0 +1,139 @@ +package me.rhunk.snapenhance.core.util.download + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import me.rhunk.snapenhance.core.Logger +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.io.PrintWriter +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketException +import java.util.Locale +import java.util.StringTokenizer +import java.util.concurrent.ConcurrentHashMap +import kotlin.random.Random + +class HttpServer( + private val timeout: Int = 10000 +) { + val port = Random.nextInt(10000, 65535) + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + private var timeoutJob: Job? = null + private var socketJob: Job? = null + + private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>() + private var serverSocket: ServerSocket? = null + + fun ensureServerStarted(callback: HttpServer.() -> Unit) { + if (serverSocket != null && !serverSocket!!.isClosed) { + callback(this) + return + } + + coroutineScope.launch(Dispatchers.IO) { + Logger.directDebug("starting http server on port $port") + serverSocket = ServerSocket(port) + callback(this@HttpServer) + while (!serverSocket!!.isClosed) { + try { + val socket = serverSocket!!.accept() + timeoutJob?.cancel() + launch { + handleRequest(socket) + timeoutJob = launch { + delay(timeout.toLong()) + Logger.directDebug("http server closed due to timeout") + runCatching { + socketJob?.cancel() + socket.close() + serverSocket?.close() + }.onFailure { + Logger.directError("failed to close socket", it) + } + } + } + } catch (e: SocketException) { + Logger.directDebug("http server timed out") + break; + } catch (e: Throwable) { + Logger.directError("failed to handle request", e) + } + } + }.also { socketJob = it } + } + + fun close() { + serverSocket?.close() + } + + fun putDownloadableContent(inputStream: InputStream, size: Long): String { + val key = System.nanoTime().toString(16) + cachedData[key] = inputStream to size + return "http://127.0.0.1:$port/$key" + } + + private fun handleRequest(socket: Socket) { + val reader = BufferedReader(InputStreamReader(socket.getInputStream())) + val outputStream = socket.getOutputStream() + val writer = PrintWriter(outputStream) + val line = reader.readLine() ?: return + fun close() { + runCatching { + reader.close() + writer.close() + outputStream.close() + socket.close() + }.onFailure { + Logger.directError("failed to close socket", it) + } + } + val parse = StringTokenizer(line) + val method = parse.nextToken().uppercase(Locale.getDefault()) + var fileRequested = parse.nextToken().lowercase(Locale.getDefault()) + Logger.directDebug("[http-server:${port}] $method $fileRequested") + + if (method != "GET") { + with(writer) { + println("HTTP/1.1 501 Not Implemented") + println("Content-type: " + "application/octet-stream") + println("Content-length: " + 0) + println() + flush() + } + close() + return + } + if (fileRequested.startsWith("/")) { + fileRequested = fileRequested.substring(1) + } + if (!cachedData.containsKey(fileRequested)) { + with(writer) { + println("HTTP/1.1 404 Not Found") + println("Content-type: " + "application/octet-stream") + println("Content-length: " + 0) + println() + flush() + } + close() + return + } + val requestedData = cachedData[fileRequested]!! + with(writer) { + println("HTTP/1.1 200 OK") + println("Content-type: " + "application/octet-stream") + println("Content-length: " + requestedData.second) + println() + flush() + } + requestedData.first.copyTo(outputStream) + outputStream.flush() + cachedData.remove(fileRequested) + close() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/download/RemoteMediaResolver.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/download/RemoteMediaResolver.kt @@ -0,0 +1,53 @@ +package me.rhunk.snapenhance.core.util.download + +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.Logger +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.util.Base64 + +object RemoteMediaResolver { + private const val BOLT_HTTP_RESOLVER_URL = "https://aws.api.snapchat.com/bolt-http" + const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/" + + private val urlCache = mutableMapOf<String, String>() + + private val okHttpClient = OkHttpClient.Builder() + .followRedirects(true) + .retryOnConnectionFailure(true) + .readTimeout(20, java.util.concurrent.TimeUnit.SECONDS) + .addInterceptor { chain -> + val request = chain.request() + val requestUrl = request.url.toString() + + if (urlCache.containsKey(requestUrl)) { + val cachedUrl = urlCache[requestUrl]!! + return@addInterceptor chain.proceed(request.newBuilder().url(cachedUrl).build()) + } + + chain.proceed(request).apply { + val responseUrl = this.request.url.toString() + if (responseUrl.startsWith("https://cf-st.sc-cdn.net")) { + urlCache[requestUrl] = responseUrl + } + } + } + .build() + + fun downloadBoltMedia(protoKey: ByteArray): InputStream? { + val request = Request.Builder() + .url(BOLT_HTTP_RESOLVER_URL + "/resolve?co=" + Base64.getUrlEncoder().encodeToString(protoKey)) + .addHeader("User-Agent", Constants.USER_AGENT) + .build() + + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + Logger.directDebug("Unexpected code $response") + return null + } + return ByteArrayInputStream(response.body.bytes()) + } + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/export/MessageExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/export/MessageExporter.kt @@ -0,0 +1,342 @@ +package me.rhunk.snapenhance.core.util.export + +import android.os.Environment +import android.util.Base64InputStream +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import de.robv.android.xposed.XposedHelpers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry +import me.rhunk.snapenhance.core.database.objects.FriendInfo +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.util.snap.EncryptionHelper +import me.rhunk.snapenhance.core.util.snap.MediaDownloaderHelper +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.data.MediaReferenceType +import me.rhunk.snapenhance.data.wrapper.impl.Message +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.Base64 +import java.util.Collections +import java.util.Date +import java.util.Locale +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.zip.Deflater +import java.util.zip.DeflaterInputStream +import java.util.zip.ZipFile +import kotlin.io.encoding.ExperimentalEncodingApi + + +enum class ExportFormat( + val extension: String, +){ + JSON("json"), + TEXT("txt"), + HTML("html"); +} + +class MessageExporter( + private val context: ModContext, + private val outputFile: File, + private val friendFeedEntry: FriendFeedEntry, + private val mediaToDownload: List<ContentType>? = null, + private val printLog: (String) -> Unit = {}, +) { + private lateinit var conversationParticipants: Map<String, FriendInfo> + private lateinit var messages: List<Message> + + fun readMessages(messages: List<Message>) { + conversationParticipants = + context.database.getConversationParticipants(friendFeedEntry.key!!) + ?.mapNotNull { + context.database.getFriendInfo(it) + }?.associateBy { it.userId!! } ?: emptyMap() + + if (conversationParticipants.isEmpty()) + throw Throwable("Failed to get conversation participants for ${friendFeedEntry.key}") + + this.messages = messages.sortedBy { it.orderKey } + } + + private fun serializeMessageContent(message: Message): String? { + return if (message.messageContent.contentType == ContentType.CHAT) { + ProtoReader(message.messageContent.content).getString(2, 1) ?: "Failed to parse message" + } else null + } + + private fun exportText(output: OutputStream) { + val writer = output.bufferedWriter() + writer.write("Conversation key: ${friendFeedEntry.key}\n") + writer.write("Conversation Name: ${friendFeedEntry.feedDisplayName}\n") + writer.write("Participants:\n") + conversationParticipants.forEach { (userId, friendInfo) -> + writer.write(" $userId: ${friendInfo.displayName}\n") + } + + writer.write("\nMessages:\n") + messages.forEach { message -> + val sender = conversationParticipants[message.senderId.toString()] + val senderUsername = sender?.usernameForSorting ?: message.senderId.toString() + val senderDisplayName = sender?.displayName ?: message.senderId.toString() + val messageContent = serializeMessageContent(message) ?: message.messageContent.contentType?.name + val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).format(Date(message.messageMetadata.createdAt)) + writer.write("[$date] - $senderDisplayName (${senderUsername}): $messageContent\n") + } + writer.flush() + } + + @OptIn(ExperimentalEncodingApi::class) + suspend fun exportHtml(output: OutputStream) { + val downloadMediaCacheFolder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "SnapEnhance/cache").also { it.mkdirs() } + val mediaFiles = Collections.synchronizedMap(mutableMapOf<String, Pair<FileType, File>>()) + val threadPool = Executors.newFixedThreadPool(15) + + withContext(Dispatchers.IO) { + var processCount = 0 + + fun updateProgress(type: String) { + val total = messages.filter { + mediaToDownload?.contains(it.messageContent.contentType) ?: false + }.size + processCount++ + printLog("$type $processCount/$total") + } + + messages.filter { + mediaToDownload?.contains(it.messageContent.contentType) ?: false + }.forEach { message -> + threadPool.execute { + val remoteMediaReferences by lazy { + val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject + serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + } + + remoteMediaReferences.firstOrNull().takeIf { it != null }?.let { media -> + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + + runCatching { + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference) { + EncryptionHelper.decryptInputStream(it, message.messageContent.contentType!!, ProtoReader(message.messageContent.content), isArroyo = false) + } + + downloadedMedia.forEach { (type, mediaData) -> + val fileType = FileType.fromByteArray(mediaData) + val fileName = "${type}_${kotlin.io.encoding.Base64.UrlSafe.encode(protoMediaReference).replace("=", "")}" + + val mediaFile = File(downloadMediaCacheFolder, "$fileName.${fileType.fileExtension}") + + FileOutputStream(mediaFile).use { fos -> + mediaData.inputStream().copyTo(fos) + } + + mediaFiles[fileName] = fileType to mediaFile + updateProgress("downloaded") + } + }.onFailure { + printLog("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}") + context.log.error("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}", it) + } + } + } + } + + threadPool.shutdown() + threadPool.awaitTermination(30, TimeUnit.DAYS) + processCount = 0 + + printLog("writing downloaded medias...") + + //write the head of the html file + output.write(""" + <!DOCTYPE html> + <html> + <head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title></title> + </head> + """.trimIndent().toByteArray()) + + output.write("<!-- This file was generated by SnapEnhance ${BuildConfig.VERSION_NAME} -->\n".toByteArray()) + + mediaFiles.forEach { (key, filePair) -> + output.write("<div class=\"media-$key\"><!-- ".toByteArray()) + + val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true)) + val base64InputStream = XposedHelpers.newInstance( + Base64InputStream::class.java, + deflateInputStream, + android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, + true + ) as InputStream + base64InputStream.copyTo(output) + deflateInputStream.close() + + output.write(" --></div>\n".toByteArray()) + output.flush() + updateProgress("wrote") + } + printLog("writing json conversation data...") + + //write the json file + output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray()) + exportJson(output) + output.write("</script>\n".toByteArray()) + + printLog("writing template...") + + runCatching { + ZipFile(context.bridgeClient.getApplicationApkPath()).use { apkFile -> + //export rawinflate.js + apkFile.getEntry("assets/web/rawinflate.js").let { entry -> + output.write("<script>".toByteArray()) + apkFile.getInputStream(entry).copyTo(output) + output.write("</script>\n".toByteArray()) + } + + //export avenir next font + apkFile.getEntry("res/font/avenir_next_medium.ttf").let { entry -> + val encodedFontData = kotlin.io.encoding.Base64.Default.encode(apkFile.getInputStream(entry).readBytes()) + output.write(""" + <style> + @font-face { + font-family: 'Avenir Next'; + src: url('data:font/truetype;charset=utf-8;base64, $encodedFontData'); + font-weight: normal; + font-style: normal; + } + </style> + """.trimIndent().toByteArray()) + } + + apkFile.getEntry("assets/web/export_template.html").let { entry -> + apkFile.getInputStream(entry).copyTo(output) + } + + apkFile.close() + } + }.onFailure { + throw Throwable("Failed to read template from apk", it) + } + + output.write("</html>".toByteArray()) + output.close() + } + } + + private fun exportJson(output: OutputStream) { + val rootObject = JsonObject().apply { + addProperty("conversationId", friendFeedEntry.key) + addProperty("conversationName", friendFeedEntry.feedDisplayName) + + var index = 0 + val participants = mutableMapOf<String, Int>() + + add("participants", JsonObject().apply { + conversationParticipants.forEach { (userId, friendInfo) -> + add(userId, JsonObject().apply { + addProperty("id", index) + addProperty("displayName", friendInfo.displayName) + addProperty("username", friendInfo.usernameForSorting) + addProperty("bitmojiSelfieId", friendInfo.bitmojiSelfieId) + }) + participants[userId] = index++ + } + }) + add("messages", JsonArray().apply { + messages.forEach { message -> + add(JsonObject().apply { + addProperty("orderKey", message.orderKey) + addProperty("senderId", participants.getOrDefault(message.senderId.toString(), -1)) + addProperty("type", message.messageContent.contentType.toString()) + + fun addUUIDList(name: String, list: List<SnapUUID>) { + add(name, JsonArray().apply { + list.map { participants.getOrDefault(it.toString(), -1) }.forEach { add(it) } + }) + } + + addUUIDList("savedBy", message.messageMetadata.savedBy) + addUUIDList("seenBy", message.messageMetadata.seenBy) + addUUIDList("openedBy", message.messageMetadata.openedBy) + + add("reactions", JsonObject().apply { + message.messageMetadata.reactions.forEach { reaction -> + addProperty( + participants.getOrDefault(reaction.userId.toString(), -1L).toString(), + reaction.reactionId + ) + } + }) + + addProperty("createdTimestamp", message.messageMetadata.createdAt) + addProperty("readTimestamp", message.messageMetadata.readAt) + addProperty("serializedContent", serializeMessageContent(message)) + addProperty("rawContent", Base64.getUrlEncoder().encodeToString(message.messageContent.content)) + + val messageContentType = message.messageContent.contentType ?: ContentType.CHAT + + EncryptionHelper.getEncryptionKeys(messageContentType, ProtoReader(message.messageContent.content), isArroyo = false)?.let { encryptionKeyPair -> + add("encryption", JsonObject().apply encryption@{ + addProperty("key", Base64.getEncoder().encodeToString(encryptionKeyPair.first)) + addProperty("iv", Base64.getEncoder().encodeToString(encryptionKeyPair.second)) + }) + } + + val remoteMediaReferences by lazy { + val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject + serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + } + + add("mediaReferences", JsonArray().apply mediaReferences@ { + if (messageContentType != ContentType.EXTERNAL_MEDIA && + messageContentType != ContentType.STICKER && + messageContentType != ContentType.SNAP && + messageContentType != ContentType.NOTE) + return@mediaReferences + + remoteMediaReferences.forEach { media -> + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) + add(JsonObject().apply { + addProperty("mediaType", mediaType.toString()) + addProperty("content", Base64.getUrlEncoder().encodeToString(protoMediaReference)) + }) + } + }) + + }) + } + }) + } + + output.write(context.gson.toJson(rootObject).toByteArray()) + output.flush() + } + + suspend fun exportTo(exportFormat: ExportFormat) { + val output = FileOutputStream(outputFile) + + when (exportFormat) { + ExportFormat.HTML -> exportHtml(output) + ExportFormat.JSON -> exportJson(output) + ExportFormat.TEXT -> exportText(output) + } + + output.close() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidCompatExtensions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidCompatExtensions.kt @@ -0,0 +1,13 @@ +package me.rhunk.snapenhance.core.util.ktx + +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.os.Build + +fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getApplicationInfo(packageName, ApplicationInfoFlags.of(flags.toLong())) + } else { + @Suppress("DEPRECATION") + getApplicationInfo(packageName, flags) + } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/DbCursorExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/DbCursorExt.kt @@ -0,0 +1,37 @@ +package me.rhunk.snapenhance.core.util.ktx + +import android.database.Cursor + +fun Cursor.getStringOrNull(columnName: String): String? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getString(columnIndex) +} + +fun Cursor.getIntOrNull(columnName: String): Int? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getInt(columnIndex) +} + +fun Cursor.getInteger(columnName: String) = getIntOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") +fun Cursor.getLong(columnName: String) = getLongOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") + +fun Cursor.getBlobOrNull(columnName: String): ByteArray? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getBlob(columnIndex) +} + + +fun Cursor.getLongOrNull(columnName: String): Long? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getLong(columnIndex) +} + +fun Cursor.getDoubleOrNull(columnName: String): Double? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getDouble(columnIndex) +} + +fun Cursor.getFloatOrNull(columnName: String): Float? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getFloat(columnIndex) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/XposedHelperExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/XposedHelperExt.kt @@ -0,0 +1,27 @@ +package me.rhunk.snapenhance.core.util.ktx + +import de.robv.android.xposed.XposedHelpers + +fun Any.getObjectField(fieldName: String): Any? { + return XposedHelpers.getObjectField(this, fieldName) +} + +fun Any.setEnumField(fieldName: String, value: String) { + this::class.java.getDeclaredField(fieldName) + .type.enumConstants?.firstOrNull { it.toString() == value }?.let { enum -> + setObjectField(fieldName, enum) + } +} + +fun Any.setObjectField(fieldName: String, value: Any?) { + XposedHelpers.setObjectField(this, fieldName, value) +} + +fun Any.getObjectFieldOrNull(fieldName: String): Any? { + return try { + getObjectField(fieldName) + } catch (e: Exception) { + null + } +} + diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoEditor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoEditor.kt @@ -0,0 +1,67 @@ +package me.rhunk.snapenhance.core.util.protobuf + + +typealias WireCallback = EditorContext.() -> Unit + +class EditorContext( + private val wires: MutableMap<Int, MutableList<Wire>> +) { + fun clear() { + wires.clear() + } + fun addWire(wire: Wire) { + wires.getOrPut(wire.id) { mutableListOf() }.add(wire) + } + fun addVarInt(id: Int, value: Int) = addVarInt(id, value.toLong()) + fun addVarInt(id: Int, value: Long) = addWire(Wire(id, WireType.VARINT, value)) + fun addBuffer(id: Int, value: ByteArray) = addWire(Wire(id, WireType.LENGTH_DELIMITED, value)) + fun add(id: Int, content: ProtoWriter.() -> Unit) = addBuffer(id, ProtoWriter().apply(content).toByteArray()) + fun addString(id: Int, value: String) = addBuffer(id, value.toByteArray()) + fun addFixed64(id: Int, value: Long) = addWire(Wire(id, WireType.FIXED64, value)) + fun addFixed32(id: Int, value: Int) = addWire(Wire(id, WireType.FIXED32, value)) + + fun firstOrNull(id: Int) = wires[id]?.firstOrNull() + fun getOrNull(id: Int) = wires[id] + fun get(id: Int) = wires[id]!! + + fun remove(id: Int) = wires.remove(id) + fun remove(id: Int, index: Int) = wires[id]?.removeAt(index) +} + +class ProtoEditor( + private var buffer: ByteArray +) { + fun edit(vararg path: Int, callback: WireCallback) { + buffer = writeAtPath(path, 0, ProtoReader(buffer), callback) + } + + private fun writeAtPath(path: IntArray, currentIndex: Int, rootReader: ProtoReader, wireToWriteCallback: WireCallback): ByteArray { + val id = path.getOrNull(currentIndex) + val output = ProtoWriter() + val wires = mutableMapOf<Int, MutableList<Wire>>() + + rootReader.forEach { wireId, value -> + wires.putIfAbsent(wireId, mutableListOf()) + if (id != null && wireId == id) { + val childReader = rootReader.followPath(id) + if (childReader == null) { + wires.getOrPut(wireId) { mutableListOf() }.add(value) + return@forEach + } + wires[wireId]!!.add(Wire(wireId, WireType.LENGTH_DELIMITED, writeAtPath(path, currentIndex + 1, childReader, wireToWriteCallback))) + return@forEach + } + wires[wireId]!!.add(value) + } + + if (currentIndex == path.size) { + wireToWriteCallback(EditorContext(wires)) + } + + wires.values.flatten().forEach(output::addWire) + + return output.toByteArray() + } + + fun toByteArray() = buffer +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoReader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoReader.kt @@ -0,0 +1,175 @@ +package me.rhunk.snapenhance.core.util.protobuf + +data class Wire(val id: Int, val type: WireType, val value: Any) + +class ProtoReader(private val buffer: ByteArray) { + private var offset: Int = 0 + private val values = mutableMapOf<Int, MutableList<Wire>>() + + init { + read() + } + + fun getBuffer() = buffer + + private fun readByte() = buffer[offset++] + + private fun readVarInt(): Long { + var result = 0L + var shift = 0 + while (true) { + val b = readByte() + result = result or ((b.toLong() and 0x7F) shl shift) + if (b.toInt() and 0x80 == 0) { + break + } + shift += 7 + } + return result + } + + private fun read() { + while (offset < buffer.size) { + val tag = readVarInt().toInt() + val id = tag ushr 3 + val type = WireType.fromValue(tag and 0x7) ?: break + try { + val value = when (type) { + WireType.VARINT -> readVarInt() + WireType.FIXED64 -> { + val bytes = ByteArray(8) + for (i in 0..7) { + bytes[i] = readByte() + } + bytes + } + WireType.LENGTH_DELIMITED -> { + val length = readVarInt().toInt() + val bytes = ByteArray(length) + for (i in 0 until length) { + bytes[i] = readByte() + } + bytes + } + WireType.START_GROUP -> { + val bytes = mutableListOf<Byte>() + while (true) { + val b = readByte() + if (b.toInt() == WireType.END_GROUP.value) { + break + } + bytes.add(b) + } + bytes.toByteArray() + } + WireType.FIXED32 -> { + val bytes = ByteArray(4) + for (i in 0..3) { + bytes[i] = readByte() + } + bytes + } + WireType.END_GROUP -> continue + } + values.getOrPut(id) { mutableListOf() }.add(Wire(id, type, value)) + } catch (t: Throwable) { + values.clear() + break + } + } + } + + fun followPath(vararg ids: Int, excludeLast: Boolean = false, reader: (ProtoReader.() -> Unit)? = null): ProtoReader? { + var thisReader = this + ids.let { + if (excludeLast) { + it.sliceArray(0 until it.size - 1) + } else { + it + } + }.forEach { id -> + if (!thisReader.contains(id)) { + return null + } + thisReader = ProtoReader(thisReader.getByteArray(id) ?: return null) + } + if (reader != null) { + thisReader.reader() + } + return thisReader + } + + fun containsPath(vararg ids: Int): Boolean { + var thisReader = this + ids.forEach { id -> + if (!thisReader.contains(id)) { + return false + } + thisReader = ProtoReader(thisReader.getByteArray(id) ?: return false) + } + return true + } + + fun forEach(reader: (Int, Wire) -> Unit) { + values.forEach { (id, wires) -> + wires.forEach { wire -> + reader(id, wire) + } + } + } + + fun forEach(vararg id: Int, reader: ProtoReader.() -> Unit) { + followPath(*id)?.eachBuffer { _, buffer -> + ProtoReader(buffer).reader() + } + } + + fun eachBuffer(vararg ids: Int, reader: ProtoReader.() -> Unit) { + followPath(*ids, excludeLast = true)?.eachBuffer { id, buffer -> + if (id == ids.last()) { + ProtoReader(buffer).reader() + } + } + } + + fun eachBuffer(reader: (Int, ByteArray) -> Unit) { + values.forEach { (id, wires) -> + wires.forEach { wire -> + if (wire.type == WireType.LENGTH_DELIMITED) { + reader(id, wire.value as ByteArray) + } + } + } + } + + fun contains(id: Int) = values.containsKey(id) + + fun getWire(id: Int) = values[id]?.firstOrNull() + fun getRawValue(id: Int) = getWire(id)?.value + fun getByteArray(id: Int) = getRawValue(id) as? ByteArray + fun getByteArray(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getByteArray(ids.last()) + fun getString(id: Int) = getByteArray(id)?.toString(Charsets.UTF_8) + fun getString(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getString(ids.last()) + fun getVarInt(id: Int) = getRawValue(id) as? Long + fun getVarInt(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getVarInt(ids.last()) + fun getCount(id: Int) = values[id]?.size ?: 0 + + fun getFixed64(id: Int): Long { + val bytes = getByteArray(id) ?: return 0L + var value = 0L + for (i in 0..7) { + value = value or ((bytes[i].toLong() and 0xFF) shl (i * 8)) + } + return value + } + + + fun getFixed32(id: Int): Int { + val bytes = getByteArray(id) ?: return 0 + var value = 0 + for (i in 0..3) { + value = value or ((bytes[i].toInt() and 0xFF) shl (i * 8)) + } + return value + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoWriter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/ProtoWriter.kt @@ -0,0 +1,117 @@ +package me.rhunk.snapenhance.core.util.protobuf + +import java.io.ByteArrayOutputStream + +class ProtoWriter { + private val stream: ByteArrayOutputStream = ByteArrayOutputStream() + + private fun writeVarInt(value: Int) { + var v = value + while (v and -0x80 != 0) { + stream.write(v and 0x7F or 0x80) + v = v ushr 7 + } + stream.write(v) + } + + private fun writeVarLong(value: Long) { + var v = value + while (v and -0x80L != 0L) { + stream.write((v and 0x7FL or 0x80L).toInt()) + v = v ushr 7 + } + stream.write(v.toInt()) + } + + fun addBuffer(id: Int, value: ByteArray) { + writeVarInt(id shl 3 or WireType.LENGTH_DELIMITED.value) + writeVarInt(value.size) + stream.write(value) + } + + fun addVarInt(id: Int, value: Int) = addVarInt(id, value.toLong()) + + fun addVarInt(id: Int, value: Long) { + writeVarInt(id shl 3) + writeVarLong(value) + } + + fun addString(id: Int, value: String) = addBuffer(id, value.toByteArray()) + + fun addFixed32(id: Int, value: Int) { + writeVarInt(id shl 3 or WireType.FIXED32.value) + val bytes = ByteArray(4) + for (i in 0..3) { + bytes[i] = (value shr (i * 8)).toByte() + } + stream.write(bytes) + } + + fun addFixed64(id: Int, value: Long) { + writeVarInt(id shl 3 or WireType.FIXED64.value) + val bytes = ByteArray(8) + for (i in 0..7) { + bytes[i] = (value shr (i * 8)).toByte() + } + stream.write(bytes) + } + + fun from(id: Int, writer: ProtoWriter.() -> Unit) { + val writerStream = ProtoWriter() + writer(writerStream) + addBuffer(id, writerStream.stream.toByteArray()) + } + + fun from(vararg ids: Int, writer: ProtoWriter.() -> Unit) { + val writerStream = ProtoWriter() + writer(writerStream) + var stream = writerStream.stream.toByteArray() + ids.reversed().forEach { id -> + with(ProtoWriter()) { + addBuffer(id, stream) + stream = this.stream.toByteArray() + } + } + stream.let(this.stream::write) + } + + fun addWire(wire: Wire) { + writeVarInt(wire.id shl 3 or wire.type.value) + when (wire.type) { + WireType.VARINT -> writeVarLong(wire.value as Long) + WireType.FIXED64, WireType.FIXED32 -> { + when (wire.value) { + is Int -> { + val bytes = ByteArray(4) + for (i in 0..3) { + bytes[i] = (wire.value shr (i * 8)).toByte() + } + stream.write(bytes) + } + is Long -> { + val bytes = ByteArray(8) + for (i in 0..7) { + bytes[i] = (wire.value shr (i * 8)).toByte() + } + stream.write(bytes) + } + is ByteArray -> stream.write(wire.value) + } + } + WireType.LENGTH_DELIMITED -> { + val value = wire.value as ByteArray + writeVarInt(value.size) + stream.write(value) + } + WireType.START_GROUP -> { + val value = wire.value as ByteArray + stream.write(value) + } + WireType.END_GROUP -> return + } + } + + fun toByteArray(): ByteArray { + return stream.toByteArray() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/WireType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/protobuf/WireType.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.core.util.protobuf; + +enum class WireType(val value: Int) { + VARINT(0), + FIXED64(1), + LENGTH_DELIMITED(2), + START_GROUP(3), + END_GROUP(4), + FIXED32(5); + + companion object { + fun fromValue(value: Int) = values().firstOrNull { it.value == value } + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/BitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/BitmojiSelfie.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.core.util.snap + +object BitmojiSelfie { + enum class BitmojiSelfieType( + val prefixUrl: String, + ) { + STANDARD("https://sdk.bitmoji.com/render/panel/"), + THREE_D("https://images.bitmoji.com/3d/render/") + } + + fun getBitmojiSelfie(selfieId: String?, avatarId: String?, type: BitmojiSelfieType): String? { + if (selfieId.isNullOrEmpty() || avatarId.isNullOrEmpty()) { + return null + } + return when (type) { + BitmojiSelfieType.STANDARD -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?transparent=1" + BitmojiSelfieType.THREE_D -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?trim=circle" + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/EncryptionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/EncryptionHelper.kt @@ -0,0 +1,53 @@ +package me.rhunk.snapenhance.core.util.snap + +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader +import me.rhunk.snapenhance.data.ContentType +import java.io.InputStream +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object EncryptionHelper { + fun getEncryptionKeys(contentType: ContentType, messageProto: ProtoReader, isArroyo: Boolean): Pair<ByteArray, ByteArray>? { + val mediaEncryptionInfo = MediaDownloaderHelper.getMessageMediaEncryptionInfo( + messageProto, + contentType, + isArroyo + ) ?: return null + val encryptionProtoIndex = if (mediaEncryptionInfo.contains(Constants.ENCRYPTION_PROTO_INDEX_V2)) { + Constants.ENCRYPTION_PROTO_INDEX_V2 + } else { + Constants.ENCRYPTION_PROTO_INDEX + } + val encryptionProto = mediaEncryptionInfo.followPath(encryptionProtoIndex) ?: return null + + var key: ByteArray = encryptionProto.getByteArray(1)!! + var iv: ByteArray = encryptionProto.getByteArray(2)!! + + if (encryptionProtoIndex == Constants.ENCRYPTION_PROTO_INDEX_V2) { + val decoder = Base64.getMimeDecoder() + key = decoder.decode(key) + iv = decoder.decode(iv) + } + + return Pair(key, iv) + } + + fun decryptInputStream( + inputStream: InputStream, + contentType: ContentType, + messageProto: ProtoReader, + isArroyo: Boolean + ): InputStream { + val encryptionKeys = getEncryptionKeys(contentType, messageProto, isArroyo) ?: throw Exception("Failed to get encryption keys") + + Cipher.getInstance("AES/CBC/PKCS5Padding").apply { + init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKeys.first, "AES"), IvParameterSpec(encryptionKeys.second)) + }.let { cipher -> + return CipherInputStream(inputStream, cipher) + } + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/MediaDownloaderHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/MediaDownloaderHelper.kt @@ -0,0 +1,70 @@ +package me.rhunk.snapenhance.core.util.snap + +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.download.data.SplitMediaAssetType +import me.rhunk.snapenhance.core.util.download.RemoteMediaResolver +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.FileType +import java.io.ByteArrayInputStream +import java.io.FileNotFoundException +import java.io.InputStream +import java.util.zip.ZipInputStream + + +object MediaDownloaderHelper { + fun getMessageMediaEncryptionInfo(protoReader: ProtoReader, contentType: ContentType, isArroyo: Boolean): ProtoReader? { + val messageContainerPath = if (isArroyo) protoReader.followPath(*Constants.ARROYO_MEDIA_CONTAINER_PROTO_PATH)!! else protoReader + val mediaContainerPath = if (contentType == ContentType.NOTE) intArrayOf(6, 1, 1) else intArrayOf(5, 1, 1) + + return when (contentType) { + ContentType.NOTE -> messageContainerPath.followPath(*mediaContainerPath) + ContentType.SNAP -> messageContainerPath.followPath(*(intArrayOf(11) + mediaContainerPath)) + ContentType.EXTERNAL_MEDIA -> { + val externalMediaTypes = arrayOf( + intArrayOf(3, 3, *mediaContainerPath), //normal external media + intArrayOf(7, 15, 1, 1), //attached audio note + intArrayOf(7, 12, 3, *mediaContainerPath), //attached story reply + intArrayOf(7, 3, *mediaContainerPath), //original story reply + ) + externalMediaTypes.forEach { path -> + messageContainerPath.followPath(*path)?.also { return it } + } + null + } + else -> null + } + } + + fun downloadMediaFromReference( + mediaReference: ByteArray, + decryptionCallback: (InputStream) -> InputStream, + ): Map<SplitMediaAssetType, ByteArray> { + val inputStream = RemoteMediaResolver.downloadBoltMedia(mediaReference) ?: throw FileNotFoundException("Unable to get media key. Check the logs for more info") + val content = decryptionCallback(inputStream).readBytes() + val fileType = FileType.fromByteArray(content) + val isZipFile = fileType == FileType.ZIP + + //videos with overlay are packed in a zip file + //there are 2 files in the zip file, the video (webm) and the overlay (png) + if (isZipFile) { + var videoData: ByteArray? = null + var overlayData: ByteArray? = null + val zipInputStream = ZipInputStream(ByteArrayInputStream(content)) + while (zipInputStream.nextEntry != null) { + val zipEntryData: ByteArray = zipInputStream.readBytes() + val entryFileType = FileType.fromByteArray(zipEntryData) + if (entryFileType.isVideo) { + videoData = zipEntryData + } else if (entryFileType.isImage) { + overlayData = zipEntryData + } + } + videoData ?: throw FileNotFoundException("Unable to find video file in zip file") + overlayData ?: throw FileNotFoundException("Unable to find overlay file in zip file") + return mapOf(SplitMediaAssetType.ORIGINAL to videoData, SplitMediaAssetType.OVERLAY to overlayData) + } + + return mapOf(SplitMediaAssetType.ORIGINAL to content) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/PreviewUtils.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/PreviewUtils.kt @@ -0,0 +1,87 @@ +package me.rhunk.snapenhance.core.util.snap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Matrix +import android.media.MediaDataSource +import android.media.MediaMetadataRetriever +import me.rhunk.snapenhance.data.FileType +import java.io.File + +object PreviewUtils { + fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? { + if (!isVideo) { + return BitmapFactory.decodeByteArray(data, 0, data.size) + } + return MediaMetadataRetriever().apply { + setDataSource(object : MediaDataSource() { + override fun readAt( + position: Long, + buffer: ByteArray, + offset: Int, + size: Int + ): Int { + var newSize = size + val length = data.size + if (position >= length) { + return -1 + } + if (position + newSize > length) { + newSize = length - position.toInt() + } + System.arraycopy(data, position.toInt(), buffer, offset, newSize) + return newSize + } + + override fun getSize(): Long { + return data.size.toLong() + } + override fun close() {} + }) + }.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + } + + fun createPreviewFromFile(file: File): Bitmap? { + return if (FileType.fromFile(file).isVideo) { + MediaMetadataRetriever().apply { + setDataSource(file.absolutePath) + }.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + } else { + BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options()) + } + } + + 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 mergeBitmapOverlay(originalMedia: Bitmap, overlayLayer: Bitmap): Bitmap { + val biggestBitmap = if (originalMedia.width * originalMedia.height > overlayLayer.width * overlayLayer.height) originalMedia else overlayLayer + val smallestBitmap = if (biggestBitmap == originalMedia) overlayLayer else originalMedia + + val mergedBitmap = Bitmap.createBitmap(biggestBitmap.width, biggestBitmap.height, biggestBitmap.config) + + with(Canvas(mergedBitmap)) { + val scaleMatrix = Matrix().apply { + postScale(biggestBitmap.width.toFloat() / smallestBitmap.width.toFloat(), biggestBitmap.height.toFloat() / smallestBitmap.height.toFloat()) + } + + if (biggestBitmap == originalMedia) { + drawBitmap(originalMedia, 0f, 0f, null) + drawBitmap(overlayLayer, scaleMatrix, null) + } else { + drawBitmap(originalMedia, scaleMatrix, null) + drawBitmap(overlayLayer, 0f, 0f, null) + } + } + + return mergedBitmap + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/SnapWidgetBroadcastReceiverHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/snap/SnapWidgetBroadcastReceiverHelper.kt @@ -0,0 +1,24 @@ +package me.rhunk.snapenhance.core.util.snap + +import android.content.Intent +import me.rhunk.snapenhance.Constants + +object SnapWidgetBroadcastReceiverHelper { + private const val ACTION_WIDGET_UPDATE = "com.snap.android.WIDGET_APP_START_UPDATE_ACTION" + const val CLASS_NAME = "com.snap.widgets.core.BestFriendsWidgetProvider" + + fun create(targetAction: String, callback: Intent.() -> Unit): Intent { + with(Intent()) { + callback(this) + action = ACTION_WIDGET_UPDATE + putExtra(":)", true) + putExtra("action", targetAction) + setClassName(Constants.SNAPCHAT_PACKAGE_NAME, CLASS_NAME) + return this + } + } + + fun isIncomingIntentValid(intent: Intent): Boolean { + return intent.action == ACTION_WIDGET_UPDATE && intent.getBooleanExtra(":)", false) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.data -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger import java.io.File import java.io.InputStream diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt @@ -1,12 +1,12 @@ package me.rhunk.snapenhance.data import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.core.util.CallbackBuilder +import me.rhunk.snapenhance.core.util.protobuf.ProtoWriter import me.rhunk.snapenhance.data.wrapper.AbstractWrapper import me.rhunk.snapenhance.data.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.util.CallbackBuilder -import me.rhunk.snapenhance.util.protobuf.ProtoWriter class MessageSender( private val context: ModContext, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.data.wrapper import de.robv.android.xposed.XposedHelpers -import me.rhunk.snapenhance.util.CallbackBuilder +import me.rhunk.snapenhance.core.util.CallbackBuilder import kotlin.reflect.KProperty abstract class AbstractWrapper( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.MessageState import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField class Message(obj: Any?) : AbstractWrapper(obj) { val orderKey get() = instanceNonNull().getObjectField("mOrderKey") as Long diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt @@ -1,9 +1,9 @@ package me.rhunk.snapenhance.data.wrapper.impl +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.ktx.setObjectField class MessageContent(obj: Any?) : AbstractWrapper(obj) { var content diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.data.wrapper.impl +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField class MessageDescriptor(obj: Any?) : AbstractWrapper(obj) { val messageId: Long get() = instanceNonNull().getObjectField("mMessageId") as Long diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.ktx.setObjectField @Suppress("UNCHECKED_CAST") class MessageDestinations(obj: Any) : AbstractWrapper(obj){ diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.PlayableSnapState import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField class MessageMetadata(obj: Any?) : AbstractWrapper(obj){ val createdAt: Long get() = instanceNonNull().getObjectField("mCreatedAt") as Long diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.SnapEnhance +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField import java.nio.ByteBuffer import java.util.UUID diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.data.wrapper.impl +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField class UserIdToReaction(obj: Any?) : AbstractWrapper(obj) { val userId = SnapUUID(instanceNonNull().getObjectField("mUserId")) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl.media import android.os.Parcelable +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ktx.getObjectField import java.lang.reflect.Field diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.data.wrapper.impl.media.opera +import me.rhunk.snapenhance.core.util.ReflectionHelper import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ReflectionHelper class Layer(obj: Any?) : AbstractWrapper(obj) { val paramMap: ParamMap diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl.media.opera import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.core.util.ReflectionHelper import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ReflectionHelper import java.lang.reflect.Field import java.util.concurrent.ConcurrentHashMap diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl.media.opera +import me.rhunk.snapenhance.core.util.ReflectionHelper +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.ReflectionHelper -import me.rhunk.snapenhance.util.ktx.getObjectField import java.lang.reflect.Field import java.util.concurrent.ConcurrentHashMap diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt @@ -1,11 +1,11 @@ package me.rhunk.snapenhance.features.impl import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.ktx.setObjectField class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.features.impl import me.rhunk.snapenhance.core.eventbus.events.impl.OnSnapInteractionEvent +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams @@ -8,7 +9,6 @@ import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.ktx.getObjectField class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { lateinit var conversationManager: Any diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -16,6 +16,13 @@ import me.rhunk.snapenhance.core.download.data.MediaDownloadSource import me.rhunk.snapenhance.core.download.data.SplitMediaAssetType import me.rhunk.snapenhance.core.download.data.toKeyPair import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.core.util.download.RemoteMediaResolver +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie +import me.rhunk.snapenhance.core.util.snap.EncryptionHelper +import me.rhunk.snapenhance.core.util.snap.MediaDownloaderHelper +import me.rhunk.snapenhance.core.util.snap.PreviewUtils import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo @@ -31,13 +38,6 @@ import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import me.rhunk.snapenhance.util.download.RemoteMediaResolver -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.protobuf.ProtoReader -import me.rhunk.snapenhance.util.snap.BitmojiSelfie -import me.rhunk.snapenhance.util.snap.EncryptionHelper -import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper -import me.rhunk.snapenhance.util.snap.PreviewUtils import java.nio.file.Paths import java.text.SimpleDateFormat import java.util.Locale diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/ProfilePictureDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/ProfilePictureDownloader.kt @@ -5,12 +5,12 @@ import android.widget.Button import android.widget.RelativeLayout import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import me.rhunk.snapenhance.util.protobuf.ProtoReader import java.nio.ByteBuffer class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt @@ -1,10 +1,10 @@ package me.rhunk.snapenhance.features.impl.experiments +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hookConstructor -import me.rhunk.snapenhance.util.ktx.setObjectField class UnlimitedMultiSnap : Feature("UnlimitedMultiSnap", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt @@ -2,9 +2,9 @@ package me.rhunk.snapenhance.features.impl.spying import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent +import me.rhunk.snapenhance.core.util.download.HttpServer import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.util.download.HttpServer import kotlin.coroutines.suspendCoroutine class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt @@ -1,7 +1,9 @@ package me.rhunk.snapenhance.features.impl.tweaks -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.core.util.CallbackBuilder +import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.data.MessageState import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID @@ -12,8 +14,6 @@ import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.util.CallbackBuilder -import me.rhunk.snapenhance.util.ktx.getObjectField import java.util.concurrent.Executors class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt @@ -8,13 +8,13 @@ import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics.Key import android.hardware.camera2.CameraManager import android.util.Range +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.data.wrapper.impl.ScSize import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.hook.hookConstructor -import me.rhunk.snapenhance.util.ktx.setObjectField class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { companion object { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableReplayInFF.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableReplayInFF.kt @@ -1,11 +1,11 @@ package me.rhunk.snapenhance.features.impl.tweaks +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.ktx.setEnumField import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hookConstructor -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.ktx.setEnumField class DisableReplayInFF : Feature("DisableReplayInFF", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt @@ -12,9 +12,16 @@ import android.os.Bundle import android.os.UserHandle import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.download.data.SplitMediaAssetType import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent +import me.rhunk.snapenhance.core.util.CallbackBuilder +import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.util.snap.EncryptionHelper +import me.rhunk.snapenhance.core.util.snap.MediaDownloaderHelper +import me.rhunk.snapenhance.core.util.snap.PreviewUtils +import me.rhunk.snapenhance.core.util.snap.SnapWidgetBroadcastReceiverHelper import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MediaReferenceType import me.rhunk.snapenhance.data.wrapper.impl.Message @@ -26,13 +33,6 @@ import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.CallbackBuilder -import me.rhunk.snapenhance.util.ktx.setObjectField -import me.rhunk.snapenhance.util.protobuf.ProtoReader -import me.rhunk.snapenhance.util.snap.EncryptionHelper -import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper -import me.rhunk.snapenhance.util.snap.PreviewUtils -import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { companion object{ diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/OldBitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/OldBitmojiSelfie.kt @@ -1,9 +1,9 @@ package me.rhunk.snapenhance.features.impl.tweaks import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent +import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.util.snap.BitmojiSelfie class OldBitmojiSelfie : Feature("OldBitmojiSelfie", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SendOverride.kt @@ -3,13 +3,13 @@ package me.rhunk.snapenhance.features.impl.tweaks import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.eventbus.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.core.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MessageSender import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import me.rhunk.snapenhance.util.protobuf.ProtoEditor -import me.rhunk.snapenhance.util.protobuf.ProtoReader class SendOverride : Feature("Send Override", loadParams = FeatureLoadParams.INIT_SYNC) { private var isLastSnapSavable = false diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt @@ -1,20 +1,20 @@ package me.rhunk.snapenhance.features.impl.tweaks +import me.rhunk.snapenhance.core.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MessageState import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.util.protobuf.ProtoEditor -import me.rhunk.snapenhance.util.protobuf.ProtoReader +import me.rhunk.snapenhance.hook.hookConstructor class UnlimitedSnapViewTime : Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { val state by context.config.messaging.unlimitedSnapViewTime - Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { state }) { param -> + context.classCache.message.hookConstructor(HookStage.AFTER, { state }) { param -> val message = Message(param.thisObject()) if (message.messageState != MessageState.COMMITTED) return@hookConstructor if (message.messageContent.contentType != ContentType.SNAP) return@hookConstructor diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt @@ -1,14 +1,14 @@ package me.rhunk.snapenhance.features.impl.ui import me.rhunk.snapenhance.core.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.hook.hookConstructor -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.ktx.setObjectField class PinConversations : BridgeFileFeature("PinConversations", BridgeFileType.PINNED_CONVERSATIONS, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.manager.impl -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.ConfigurationOverride @@ -34,8 +34,8 @@ import me.rhunk.snapenhance.features.impl.tweaks.OldBitmojiSelfie import me.rhunk.snapenhance.features.impl.tweaks.SendOverride import me.rhunk.snapenhance.features.impl.tweaks.SnapchatPlus import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime -import me.rhunk.snapenhance.features.impl.ui.PinConversations import me.rhunk.snapenhance.features.impl.ui.ClientBootstrapOverride +import me.rhunk.snapenhance.features.impl.ui.PinConversations import me.rhunk.snapenhance.features.impl.ui.UITweaks import me.rhunk.snapenhance.manager.Manager import me.rhunk.snapenhance.ui.menu.impl.MenuViewInjector diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -14,6 +14,7 @@ import android.widget.Switch import me.rhunk.snapenhance.core.database.objects.ConversationMessage import me.rhunk.snapenhance.core.database.objects.FriendInfo import me.rhunk.snapenhance.core.database.objects.UserConversationLink +import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging @@ -22,7 +23,6 @@ import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.features.impl.tweaks.AutoSave import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.menu.AbstractMenu -import me.rhunk.snapenhance.util.snap.BitmojiSelfie import java.net.HttpURLConnection import java.net.URL import java.text.DateFormat diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt @@ -1,93 +0,0 @@ -package me.rhunk.snapenhance.util - -import de.robv.android.xposed.XC_MethodHook -import me.rhunk.snapenhance.hook.HookAdapter -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import java.lang.reflect.Constructor -import java.lang.reflect.Field -import java.lang.reflect.Modifier - -class CallbackBuilder( - private val callbackClass: Class<*> -) { - internal class Override( - val methodName: String, - val shouldUnhook: Boolean = true, - val callback: (HookAdapter) -> Unit - ) - - private val methodOverrides = mutableListOf<Override>() - - fun override(methodName: String, shouldUnhook: Boolean = true, callback: (HookAdapter) -> Unit = {}): CallbackBuilder { - methodOverrides.add(Override(methodName, shouldUnhook, callback)) - return this - } - - fun build(): Any { - //get the first param of the first constructor to get the class of the invoker - val invokerClass: Class<*> = callbackClass.constructors[0].parameterTypes[0] - //get the invoker field based on the invoker class - val invokerField = callbackClass.fields.first { field: Field -> - field.type.isAssignableFrom(invokerClass) - } - //get the callback field based on the callback class - val callbackInstance = createEmptyObject(callbackClass.constructors[0])!! - val callbackInstanceHashCode: Int = callbackInstance.hashCode() - val callbackInstanceClass = callbackInstance.javaClass - - val unhooks = mutableListOf<XC_MethodHook.Unhook>() - - callbackInstanceClass.methods.forEach { method -> - if (method.declaringClass != callbackInstanceClass) return@forEach - if (Modifier.isPrivate(method.modifiers)) return@forEach - - //default hook that unhooks the callback and returns null - val defaultHook: (HookAdapter) -> Boolean = defaultHook@{ - //checking invokerField ensure that's the callback was created by the CallbackBuilder - if (invokerField.get(it.thisObject()) != null) return@defaultHook false - if ((it.thisObject() as Any).hashCode() != callbackInstanceHashCode) return@defaultHook false - it.setResult(null) - true - } - - var hook: (HookAdapter) -> Unit = { defaultHook(it) } - - //override the default hook if the method is in the override list - methodOverrides.find { it.methodName == method.name }?.run { - hook = { - if (defaultHook(it)) { - callback(it) - if (shouldUnhook) unhooks.forEach { unhook -> unhook.unhook() } - } - } - } - - unhooks.add(Hooker.hook(method, HookStage.BEFORE, hook)) - } - return callbackInstance - } - - companion object { - fun createEmptyObject(constructor: Constructor<*>): Any? { - //compute the args for the constructor with null or default primitive values - val args = constructor.parameterTypes.map { type: Class<*> -> - if (type.isPrimitive) { - when (type.name) { - "boolean" -> return@map false - "byte" -> return@map 0.toByte() - "char" -> return@map 0.toChar() - "short" -> return@map 0.toShort() - "int" -> return@map 0 - "long" -> return@map 0L - "float" -> return@map 0f - "double" -> return@map 0.0 - } - } - null - }.toTypedArray() - return constructor.newInstance(*args) - } - - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt @@ -1,119 +0,0 @@ -package me.rhunk.snapenhance.util - -import java.lang.reflect.Field -import java.lang.reflect.Method -import java.util.Arrays -import java.util.Objects - -object ReflectionHelper { - /** - * Searches for a field with a class that has a method with the specified name - */ - fun searchFieldWithClassMethod(clazz: Class<*>, methodName: String): Field? { - return clazz.declaredFields.firstOrNull { f: Field? -> - try { - return@firstOrNull Arrays.stream( - f!!.type.declaredMethods - ).anyMatch { method: Method -> method.name == methodName } - } catch (e: Exception) { - return@firstOrNull false - } - } - } - - fun searchFieldByType(clazz: Class<*>, type: Class<*>): Field? { - return clazz.declaredFields.firstOrNull { f: Field? -> f!!.type == type } - } - - fun searchFieldTypeInSuperClasses(clazz: Class<*>, type: Class<*>): Field? { - val field = searchFieldByType(clazz, type) - if (field != null) { - return field - } - val superclass = clazz.superclass - return superclass?.let { searchFieldTypeInSuperClasses(it, type) } - } - - fun searchFieldStartsWithToString( - clazz: Class<*>, - instance: Any, - toString: String? - ): Field? { - return clazz.declaredFields.firstOrNull { f: Field -> - try { - f.isAccessible = true - return@firstOrNull Objects.requireNonNull(f[instance]).toString() - .startsWith( - toString!! - ) - } catch (e: Throwable) { - return@firstOrNull false - } - } - } - - - fun searchFieldContainsToString( - clazz: Class<*>, - instance: Any?, - toString: String? - ): Field? { - return clazz.declaredFields.firstOrNull { f: Field -> - try { - f.isAccessible = true - return@firstOrNull Objects.requireNonNull(f[instance]).toString() - .contains(toString!!) - } catch (e: Throwable) { - return@firstOrNull false - } - } - } - - fun searchFirstFieldTypeInClassRecursive(clazz: Class<*>, type: Class<*>): Field? { - return clazz.declaredFields.firstOrNull { - val field = searchFieldByType(it.type, type) - return@firstOrNull field != null - } - } - - /** - * Searches for a field with a class that has a method with the specified return type - */ - fun searchMethodWithReturnType(clazz: Class<*>, returnType: Class<*>): Method? { - return clazz.declaredMethods.first { m: Method -> m.returnType == returnType } - } - - /** - * Searches for a field with a class that has a method with the specified return type and parameter types - */ - fun searchMethodWithParameterAndReturnType( - aClass: Class<*>, - returnType: Class<*>, - vararg parameters: Class<*> - ): Method? { - return aClass.declaredMethods.firstOrNull { m: Method -> - if (m.returnType != returnType) { - return@firstOrNull false - } - val parameterTypes = m.parameterTypes - if (parameterTypes.size != parameters.size) { - return@firstOrNull false - } - for (i in parameterTypes.indices) { - if (parameterTypes[i] != parameters[i]) { - return@firstOrNull false - } - } - true - } - } - - fun getDeclaredFieldsRecursively(clazz: Class<*>): List<Field> { - val fields = clazz.declaredFields.toMutableList() - val superclass = clazz.superclass - if (superclass != null) { - fields.addAll(getDeclaredFieldsRecursively(superclass)) - } - return fields - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt @@ -1,31 +0,0 @@ -package me.rhunk.snapenhance.util - -import android.annotation.SuppressLint -import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.Logger - -object SQLiteDatabaseHelper { - @SuppressLint("Range") - fun createTablesFromSchema(sqLiteDatabase: SQLiteDatabase, databaseSchema: Map<String, List<String>>) { - databaseSchema.forEach { (tableName, columns) -> - sqLiteDatabase.execSQL("CREATE TABLE IF NOT EXISTS $tableName (${columns.joinToString(", ")})") - - val cursor = sqLiteDatabase.rawQuery("PRAGMA table_info($tableName)", null) - val existingColumns = mutableListOf<String>() - while (cursor.moveToNext()) { - existingColumns.add(cursor.getString(cursor.getColumnIndex("name")) + " " + cursor.getString(cursor.getColumnIndex("type"))) - } - cursor.close() - - val newColumns = columns.filter { - existingColumns.none { existingColumn -> it.startsWith(existingColumn) } - } - - if (newColumns.isEmpty()) return@forEach - - Logger.directDebug("Schema for table $tableName has changed") - sqLiteDatabase.execSQL("DROP TABLE $tableName") - sqLiteDatabase.execSQL("CREATE TABLE IF NOT EXISTS $tableName (${columns.joinToString(", ")})") - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/SerializableDataObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/SerializableDataObject.kt @@ -1,22 +0,0 @@ -package me.rhunk.snapenhance.util - -import com.google.gson.Gson -import com.google.gson.GsonBuilder - -open class SerializableDataObject { - companion object { - val gson: Gson = GsonBuilder().create() - - inline fun <reified T : SerializableDataObject> fromJson(json: String): T { - return gson.fromJson(json, T::class.java) - } - - inline fun <reified T : SerializableDataObject> fromJson(json: String, type: Class<T>): T { - return gson.fromJson(json, type) - } - } - - fun toJson(): String { - return gson.toJson(this) - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt @@ -1,139 +0,0 @@ -package me.rhunk.snapenhance.util.download - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import me.rhunk.snapenhance.Logger -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader -import java.io.PrintWriter -import java.net.ServerSocket -import java.net.Socket -import java.net.SocketException -import java.util.Locale -import java.util.StringTokenizer -import java.util.concurrent.ConcurrentHashMap -import kotlin.random.Random - -class HttpServer( - private val timeout: Int = 10000 -) { - val port = Random.nextInt(10000, 65535) - - private val coroutineScope = CoroutineScope(Dispatchers.IO) - private var timeoutJob: Job? = null - private var socketJob: Job? = null - - private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>() - private var serverSocket: ServerSocket? = null - - fun ensureServerStarted(callback: HttpServer.() -> Unit) { - if (serverSocket != null && !serverSocket!!.isClosed) { - callback(this) - return - } - - coroutineScope.launch(Dispatchers.IO) { - Logger.directDebug("starting http server on port $port") - serverSocket = ServerSocket(port) - callback(this@HttpServer) - while (!serverSocket!!.isClosed) { - try { - val socket = serverSocket!!.accept() - timeoutJob?.cancel() - launch { - handleRequest(socket) - timeoutJob = launch { - delay(timeout.toLong()) - Logger.directDebug("http server closed due to timeout") - runCatching { - socketJob?.cancel() - socket.close() - serverSocket?.close() - }.onFailure { - Logger.directError("failed to close socket", it) - } - } - } - } catch (e: SocketException) { - Logger.directDebug("http server timed out") - break; - } catch (e: Throwable) { - Logger.directError("failed to handle request", e) - } - } - }.also { socketJob = it } - } - - fun close() { - serverSocket?.close() - } - - fun putDownloadableContent(inputStream: InputStream, size: Long): String { - val key = System.nanoTime().toString(16) - cachedData[key] = inputStream to size - return "http://127.0.0.1:$port/$key" - } - - private fun handleRequest(socket: Socket) { - val reader = BufferedReader(InputStreamReader(socket.getInputStream())) - val outputStream = socket.getOutputStream() - val writer = PrintWriter(outputStream) - val line = reader.readLine() ?: return - fun close() { - runCatching { - reader.close() - writer.close() - outputStream.close() - socket.close() - }.onFailure { - Logger.directError("failed to close socket", it) - } - } - val parse = StringTokenizer(line) - val method = parse.nextToken().uppercase(Locale.getDefault()) - var fileRequested = parse.nextToken().lowercase(Locale.getDefault()) - Logger.directDebug("[http-server:${port}] $method $fileRequested") - - if (method != "GET") { - with(writer) { - println("HTTP/1.1 501 Not Implemented") - println("Content-type: " + "application/octet-stream") - println("Content-length: " + 0) - println() - flush() - } - close() - return - } - if (fileRequested.startsWith("/")) { - fileRequested = fileRequested.substring(1) - } - if (!cachedData.containsKey(fileRequested)) { - with(writer) { - println("HTTP/1.1 404 Not Found") - println("Content-type: " + "application/octet-stream") - println("Content-length: " + 0) - println() - flush() - } - close() - return - } - val requestedData = cachedData[fileRequested]!! - with(writer) { - println("HTTP/1.1 200 OK") - println("Content-type: " + "application/octet-stream") - println("Content-length: " + requestedData.second) - println() - flush() - } - requestedData.first.copyTo(outputStream) - outputStream.flush() - cachedData.remove(fileRequested) - close() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt @@ -1,53 +0,0 @@ -package me.rhunk.snapenhance.util.download - -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.Logger -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.util.Base64 - -object RemoteMediaResolver { - private const val BOLT_HTTP_RESOLVER_URL = "https://aws.api.snapchat.com/bolt-http" - const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/" - - private val urlCache = mutableMapOf<String, String>() - - private val okHttpClient = OkHttpClient.Builder() - .followRedirects(true) - .retryOnConnectionFailure(true) - .readTimeout(20, java.util.concurrent.TimeUnit.SECONDS) - .addInterceptor { chain -> - val request = chain.request() - val requestUrl = request.url.toString() - - if (urlCache.containsKey(requestUrl)) { - val cachedUrl = urlCache[requestUrl]!! - return@addInterceptor chain.proceed(request.newBuilder().url(cachedUrl).build()) - } - - chain.proceed(request).apply { - val responseUrl = this.request.url.toString() - if (responseUrl.startsWith("https://cf-st.sc-cdn.net")) { - urlCache[requestUrl] = responseUrl - } - } - } - .build() - - fun downloadBoltMedia(protoKey: ByteArray): InputStream? { - val request = Request.Builder() - .url(BOLT_HTTP_RESOLVER_URL + "/resolve?co=" + Base64.getUrlEncoder().encodeToString(protoKey)) - .addHeader("User-Agent", Constants.USER_AGENT) - .build() - - okHttpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - Logger.directDebug("Unexpected code $response") - return null - } - return ByteArrayInputStream(response.body.bytes()) - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt @@ -1,342 +0,0 @@ -package me.rhunk.snapenhance.util.export - -import android.os.Environment -import android.util.Base64InputStream -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import de.robv.android.xposed.XposedHelpers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.core.BuildConfig -import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry -import me.rhunk.snapenhance.core.database.objects.FriendInfo -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.data.MediaReferenceType -import me.rhunk.snapenhance.data.wrapper.impl.Message -import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID -import me.rhunk.snapenhance.util.protobuf.ProtoReader -import me.rhunk.snapenhance.util.snap.EncryptionHelper -import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Base64 -import java.util.Collections -import java.util.Date -import java.util.Locale -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.zip.Deflater -import java.util.zip.DeflaterInputStream -import java.util.zip.ZipFile -import kotlin.io.encoding.ExperimentalEncodingApi - - -enum class ExportFormat( - val extension: String, -){ - JSON("json"), - TEXT("txt"), - HTML("html"); -} - -class MessageExporter( - private val context: ModContext, - private val outputFile: File, - private val friendFeedEntry: FriendFeedEntry, - private val mediaToDownload: List<ContentType>? = null, - private val printLog: (String) -> Unit = {}, -) { - private lateinit var conversationParticipants: Map<String, FriendInfo> - private lateinit var messages: List<Message> - - fun readMessages(messages: List<Message>) { - conversationParticipants = - context.database.getConversationParticipants(friendFeedEntry.key!!) - ?.mapNotNull { - context.database.getFriendInfo(it) - }?.associateBy { it.userId!! } ?: emptyMap() - - if (conversationParticipants.isEmpty()) - throw Throwable("Failed to get conversation participants for ${friendFeedEntry.key}") - - this.messages = messages.sortedBy { it.orderKey } - } - - private fun serializeMessageContent(message: Message): String? { - return if (message.messageContent.contentType == ContentType.CHAT) { - ProtoReader(message.messageContent.content).getString(2, 1) ?: "Failed to parse message" - } else null - } - - private fun exportText(output: OutputStream) { - val writer = output.bufferedWriter() - writer.write("Conversation key: ${friendFeedEntry.key}\n") - writer.write("Conversation Name: ${friendFeedEntry.feedDisplayName}\n") - writer.write("Participants:\n") - conversationParticipants.forEach { (userId, friendInfo) -> - writer.write(" $userId: ${friendInfo.displayName}\n") - } - - writer.write("\nMessages:\n") - messages.forEach { message -> - val sender = conversationParticipants[message.senderId.toString()] - val senderUsername = sender?.usernameForSorting ?: message.senderId.toString() - val senderDisplayName = sender?.displayName ?: message.senderId.toString() - val messageContent = serializeMessageContent(message) ?: message.messageContent.contentType?.name - val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).format(Date(message.messageMetadata.createdAt)) - writer.write("[$date] - $senderDisplayName (${senderUsername}): $messageContent\n") - } - writer.flush() - } - - @OptIn(ExperimentalEncodingApi::class) - suspend fun exportHtml(output: OutputStream) { - val downloadMediaCacheFolder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "SnapEnhance/cache").also { it.mkdirs() } - val mediaFiles = Collections.synchronizedMap(mutableMapOf<String, Pair<FileType, File>>()) - val threadPool = Executors.newFixedThreadPool(15) - - withContext(Dispatchers.IO) { - var processCount = 0 - - fun updateProgress(type: String) { - val total = messages.filter { - mediaToDownload?.contains(it.messageContent.contentType) ?: false - }.size - processCount++ - printLog("$type $processCount/$total") - } - - messages.filter { - mediaToDownload?.contains(it.messageContent.contentType) ?: false - }.forEach { message -> - threadPool.execute { - val remoteMediaReferences by lazy { - val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject - serializedMessageContent["mRemoteMediaReferences"] - .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } - .flatten() - } - - remoteMediaReferences.firstOrNull().takeIf { it != null }?.let { media -> - val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() - - runCatching { - val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference) { - EncryptionHelper.decryptInputStream(it, message.messageContent.contentType!!, ProtoReader(message.messageContent.content), isArroyo = false) - } - - downloadedMedia.forEach { (type, mediaData) -> - val fileType = FileType.fromByteArray(mediaData) - val fileName = "${type}_${kotlin.io.encoding.Base64.UrlSafe.encode(protoMediaReference).replace("=", "")}" - - val mediaFile = File(downloadMediaCacheFolder, "$fileName.${fileType.fileExtension}") - - FileOutputStream(mediaFile).use { fos -> - mediaData.inputStream().copyTo(fos) - } - - mediaFiles[fileName] = fileType to mediaFile - updateProgress("downloaded") - } - }.onFailure { - printLog("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}") - context.log.error("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}", it) - } - } - } - } - - threadPool.shutdown() - threadPool.awaitTermination(30, TimeUnit.DAYS) - processCount = 0 - - printLog("writing downloaded medias...") - - //write the head of the html file - output.write(""" - <!DOCTYPE html> - <html> - <head> - <meta charset="UTF-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title></title> - </head> - """.trimIndent().toByteArray()) - - output.write("<!-- This file was generated by SnapEnhance ${BuildConfig.VERSION_NAME} -->\n".toByteArray()) - - mediaFiles.forEach { (key, filePair) -> - output.write("<div class=\"media-$key\"><!-- ".toByteArray()) - - val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true)) - val base64InputStream = XposedHelpers.newInstance( - Base64InputStream::class.java, - deflateInputStream, - android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, - true - ) as InputStream - base64InputStream.copyTo(output) - deflateInputStream.close() - - output.write(" --></div>\n".toByteArray()) - output.flush() - updateProgress("wrote") - } - printLog("writing json conversation data...") - - //write the json file - output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray()) - exportJson(output) - output.write("</script>\n".toByteArray()) - - printLog("writing template...") - - runCatching { - ZipFile(context.bridgeClient.getApplicationApkPath()).use { apkFile -> - //export rawinflate.js - apkFile.getEntry("assets/web/rawinflate.js").let { entry -> - output.write("<script>".toByteArray()) - apkFile.getInputStream(entry).copyTo(output) - output.write("</script>\n".toByteArray()) - } - - //export avenir next font - apkFile.getEntry("res/font/avenir_next_medium.ttf").let { entry -> - val encodedFontData = kotlin.io.encoding.Base64.Default.encode(apkFile.getInputStream(entry).readBytes()) - output.write(""" - <style> - @font-face { - font-family: 'Avenir Next'; - src: url('data:font/truetype;charset=utf-8;base64, $encodedFontData'); - font-weight: normal; - font-style: normal; - } - </style> - """.trimIndent().toByteArray()) - } - - apkFile.getEntry("assets/web/export_template.html").let { entry -> - apkFile.getInputStream(entry).copyTo(output) - } - - apkFile.close() - } - }.onFailure { - throw Throwable("Failed to read template from apk", it) - } - - output.write("</html>".toByteArray()) - output.close() - } - } - - private fun exportJson(output: OutputStream) { - val rootObject = JsonObject().apply { - addProperty("conversationId", friendFeedEntry.key) - addProperty("conversationName", friendFeedEntry.feedDisplayName) - - var index = 0 - val participants = mutableMapOf<String, Int>() - - add("participants", JsonObject().apply { - conversationParticipants.forEach { (userId, friendInfo) -> - add(userId, JsonObject().apply { - addProperty("id", index) - addProperty("displayName", friendInfo.displayName) - addProperty("username", friendInfo.usernameForSorting) - addProperty("bitmojiSelfieId", friendInfo.bitmojiSelfieId) - }) - participants[userId] = index++ - } - }) - add("messages", JsonArray().apply { - messages.forEach { message -> - add(JsonObject().apply { - addProperty("orderKey", message.orderKey) - addProperty("senderId", participants.getOrDefault(message.senderId.toString(), -1)) - addProperty("type", message.messageContent.contentType.toString()) - - fun addUUIDList(name: String, list: List<SnapUUID>) { - add(name, JsonArray().apply { - list.map { participants.getOrDefault(it.toString(), -1) }.forEach { add(it) } - }) - } - - addUUIDList("savedBy", message.messageMetadata.savedBy) - addUUIDList("seenBy", message.messageMetadata.seenBy) - addUUIDList("openedBy", message.messageMetadata.openedBy) - - add("reactions", JsonObject().apply { - message.messageMetadata.reactions.forEach { reaction -> - addProperty( - participants.getOrDefault(reaction.userId.toString(), -1L).toString(), - reaction.reactionId - ) - } - }) - - addProperty("createdTimestamp", message.messageMetadata.createdAt) - addProperty("readTimestamp", message.messageMetadata.readAt) - addProperty("serializedContent", serializeMessageContent(message)) - addProperty("rawContent", Base64.getUrlEncoder().encodeToString(message.messageContent.content)) - - val messageContentType = message.messageContent.contentType ?: ContentType.CHAT - - EncryptionHelper.getEncryptionKeys(messageContentType, ProtoReader(message.messageContent.content), isArroyo = false)?.let { encryptionKeyPair -> - add("encryption", JsonObject().apply encryption@{ - addProperty("key", Base64.getEncoder().encodeToString(encryptionKeyPair.first)) - addProperty("iv", Base64.getEncoder().encodeToString(encryptionKeyPair.second)) - }) - } - - val remoteMediaReferences by lazy { - val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject - serializedMessageContent["mRemoteMediaReferences"] - .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } - .flatten() - } - - add("mediaReferences", JsonArray().apply mediaReferences@ { - if (messageContentType != ContentType.EXTERNAL_MEDIA && - messageContentType != ContentType.STICKER && - messageContentType != ContentType.SNAP && - messageContentType != ContentType.NOTE) - return@mediaReferences - - remoteMediaReferences.forEach { media -> - val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() - val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) - add(JsonObject().apply { - addProperty("mediaType", mediaType.toString()) - addProperty("content", Base64.getUrlEncoder().encodeToString(protoMediaReference)) - }) - } - }) - - }) - } - }) - } - - output.write(context.gson.toJson(rootObject).toByteArray()) - output.flush() - } - - suspend fun exportTo(exportFormat: ExportFormat) { - val output = FileOutputStream(outputFile) - - when (exportFormat) { - ExportFormat.HTML -> exportHtml(output) - ExportFormat.JSON -> exportJson(output) - ExportFormat.TEXT -> exportText(output) - } - - output.close() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/AndroidCompatExtensions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/AndroidCompatExtensions.kt @@ -1,13 +0,0 @@ -package me.rhunk.snapenhance.util.ktx - -import android.content.pm.PackageManager -import android.content.pm.PackageManager.ApplicationInfoFlags -import android.os.Build - -fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int) = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getApplicationInfo(packageName, ApplicationInfoFlags.of(flags.toLong())) - } else { - @Suppress("DEPRECATION") - getApplicationInfo(packageName, flags) - } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/DbCursorExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/DbCursorExt.kt @@ -1,37 +0,0 @@ -package me.rhunk.snapenhance.util.ktx - -import android.database.Cursor - -fun Cursor.getStringOrNull(columnName: String): String? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getString(columnIndex) -} - -fun Cursor.getIntOrNull(columnName: String): Int? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getInt(columnIndex) -} - -fun Cursor.getInteger(columnName: String) = getIntOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") -fun Cursor.getLong(columnName: String) = getLongOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") - -fun Cursor.getBlobOrNull(columnName: String): ByteArray? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getBlob(columnIndex) -} - - -fun Cursor.getLongOrNull(columnName: String): Long? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getLong(columnIndex) -} - -fun Cursor.getDoubleOrNull(columnName: String): Double? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getDouble(columnIndex) -} - -fun Cursor.getFloatOrNull(columnName: String): Float? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getFloat(columnIndex) -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/XposedHelperExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/XposedHelperExt.kt @@ -1,27 +0,0 @@ -package me.rhunk.snapenhance.util.ktx - -import de.robv.android.xposed.XposedHelpers - -fun Any.getObjectField(fieldName: String): Any? { - return XposedHelpers.getObjectField(this, fieldName) -} - -fun Any.setEnumField(fieldName: String, value: String) { - this::class.java.getDeclaredField(fieldName) - .type.enumConstants?.firstOrNull { it.toString() == value }?.let { enum -> - setObjectField(fieldName, enum) - } -} - -fun Any.setObjectField(fieldName: String, value: Any?) { - XposedHelpers.setObjectField(this, fieldName, value) -} - -fun Any.getObjectFieldOrNull(fieldName: String): Any? { - return try { - getObjectField(fieldName) - } catch (e: Exception) { - null - } -} - diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt @@ -1,67 +0,0 @@ -package me.rhunk.snapenhance.util.protobuf - - -typealias WireCallback = EditorContext.() -> Unit - -class EditorContext( - private val wires: MutableMap<Int, MutableList<Wire>> -) { - fun clear() { - wires.clear() - } - fun addWire(wire: Wire) { - wires.getOrPut(wire.id) { mutableListOf() }.add(wire) - } - fun addVarInt(id: Int, value: Int) = addVarInt(id, value.toLong()) - fun addVarInt(id: Int, value: Long) = addWire(Wire(id, WireType.VARINT, value)) - fun addBuffer(id: Int, value: ByteArray) = addWire(Wire(id, WireType.LENGTH_DELIMITED, value)) - fun add(id: Int, content: ProtoWriter.() -> Unit) = addBuffer(id, ProtoWriter().apply(content).toByteArray()) - fun addString(id: Int, value: String) = addBuffer(id, value.toByteArray()) - fun addFixed64(id: Int, value: Long) = addWire(Wire(id, WireType.FIXED64, value)) - fun addFixed32(id: Int, value: Int) = addWire(Wire(id, WireType.FIXED32, value)) - - fun firstOrNull(id: Int) = wires[id]?.firstOrNull() - fun getOrNull(id: Int) = wires[id] - fun get(id: Int) = wires[id]!! - - fun remove(id: Int) = wires.remove(id) - fun remove(id: Int, index: Int) = wires[id]?.removeAt(index) -} - -class ProtoEditor( - private var buffer: ByteArray -) { - fun edit(vararg path: Int, callback: WireCallback) { - buffer = writeAtPath(path, 0, ProtoReader(buffer), callback) - } - - private fun writeAtPath(path: IntArray, currentIndex: Int, rootReader: ProtoReader, wireToWriteCallback: WireCallback): ByteArray { - val id = path.getOrNull(currentIndex) - val output = ProtoWriter() - val wires = mutableMapOf<Int, MutableList<Wire>>() - - rootReader.forEach { wireId, value -> - wires.putIfAbsent(wireId, mutableListOf()) - if (id != null && wireId == id) { - val childReader = rootReader.followPath(id) - if (childReader == null) { - wires.getOrPut(wireId) { mutableListOf() }.add(value) - return@forEach - } - wires[wireId]!!.add(Wire(wireId, WireType.LENGTH_DELIMITED, writeAtPath(path, currentIndex + 1, childReader, wireToWriteCallback))) - return@forEach - } - wires[wireId]!!.add(value) - } - - if (currentIndex == path.size) { - wireToWriteCallback(EditorContext(wires)) - } - - wires.values.flatten().forEach(output::addWire) - - return output.toByteArray() - } - - fun toByteArray() = buffer -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt @@ -1,175 +0,0 @@ -package me.rhunk.snapenhance.util.protobuf - -data class Wire(val id: Int, val type: WireType, val value: Any) - -class ProtoReader(private val buffer: ByteArray) { - private var offset: Int = 0 - private val values = mutableMapOf<Int, MutableList<Wire>>() - - init { - read() - } - - fun getBuffer() = buffer - - private fun readByte() = buffer[offset++] - - private fun readVarInt(): Long { - var result = 0L - var shift = 0 - while (true) { - val b = readByte() - result = result or ((b.toLong() and 0x7F) shl shift) - if (b.toInt() and 0x80 == 0) { - break - } - shift += 7 - } - return result - } - - private fun read() { - while (offset < buffer.size) { - val tag = readVarInt().toInt() - val id = tag ushr 3 - val type = WireType.fromValue(tag and 0x7) ?: break - try { - val value = when (type) { - WireType.VARINT -> readVarInt() - WireType.FIXED64 -> { - val bytes = ByteArray(8) - for (i in 0..7) { - bytes[i] = readByte() - } - bytes - } - WireType.LENGTH_DELIMITED -> { - val length = readVarInt().toInt() - val bytes = ByteArray(length) - for (i in 0 until length) { - bytes[i] = readByte() - } - bytes - } - WireType.START_GROUP -> { - val bytes = mutableListOf<Byte>() - while (true) { - val b = readByte() - if (b.toInt() == WireType.END_GROUP.value) { - break - } - bytes.add(b) - } - bytes.toByteArray() - } - WireType.FIXED32 -> { - val bytes = ByteArray(4) - for (i in 0..3) { - bytes[i] = readByte() - } - bytes - } - WireType.END_GROUP -> continue - } - values.getOrPut(id) { mutableListOf() }.add(Wire(id, type, value)) - } catch (t: Throwable) { - values.clear() - break - } - } - } - - fun followPath(vararg ids: Int, excludeLast: Boolean = false, reader: (ProtoReader.() -> Unit)? = null): ProtoReader? { - var thisReader = this - ids.let { - if (excludeLast) { - it.sliceArray(0 until it.size - 1) - } else { - it - } - }.forEach { id -> - if (!thisReader.contains(id)) { - return null - } - thisReader = ProtoReader(thisReader.getByteArray(id) ?: return null) - } - if (reader != null) { - thisReader.reader() - } - return thisReader - } - - fun containsPath(vararg ids: Int): Boolean { - var thisReader = this - ids.forEach { id -> - if (!thisReader.contains(id)) { - return false - } - thisReader = ProtoReader(thisReader.getByteArray(id) ?: return false) - } - return true - } - - fun forEach(reader: (Int, Wire) -> Unit) { - values.forEach { (id, wires) -> - wires.forEach { wire -> - reader(id, wire) - } - } - } - - fun forEach(vararg id: Int, reader: ProtoReader.() -> Unit) { - followPath(*id)?.eachBuffer { _, buffer -> - ProtoReader(buffer).reader() - } - } - - fun eachBuffer(vararg ids: Int, reader: ProtoReader.() -> Unit) { - followPath(*ids, excludeLast = true)?.eachBuffer { id, buffer -> - if (id == ids.last()) { - ProtoReader(buffer).reader() - } - } - } - - fun eachBuffer(reader: (Int, ByteArray) -> Unit) { - values.forEach { (id, wires) -> - wires.forEach { wire -> - if (wire.type == WireType.LENGTH_DELIMITED) { - reader(id, wire.value as ByteArray) - } - } - } - } - - fun contains(id: Int) = values.containsKey(id) - - fun getWire(id: Int) = values[id]?.firstOrNull() - fun getRawValue(id: Int) = getWire(id)?.value - fun getByteArray(id: Int) = getRawValue(id) as? ByteArray - fun getByteArray(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getByteArray(ids.last()) - fun getString(id: Int) = getByteArray(id)?.toString(Charsets.UTF_8) - fun getString(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getString(ids.last()) - fun getVarInt(id: Int) = getRawValue(id) as? Long - fun getVarInt(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getVarInt(ids.last()) - fun getCount(id: Int) = values[id]?.size ?: 0 - - fun getFixed64(id: Int): Long { - val bytes = getByteArray(id) ?: return 0L - var value = 0L - for (i in 0..7) { - value = value or ((bytes[i].toLong() and 0xFF) shl (i * 8)) - } - return value - } - - - fun getFixed32(id: Int): Int { - val bytes = getByteArray(id) ?: return 0 - var value = 0 - for (i in 0..3) { - value = value or ((bytes[i].toInt() and 0xFF) shl (i * 8)) - } - return value - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt @@ -1,117 +0,0 @@ -package me.rhunk.snapenhance.util.protobuf - -import java.io.ByteArrayOutputStream - -class ProtoWriter { - private val stream: ByteArrayOutputStream = ByteArrayOutputStream() - - private fun writeVarInt(value: Int) { - var v = value - while (v and -0x80 != 0) { - stream.write(v and 0x7F or 0x80) - v = v ushr 7 - } - stream.write(v) - } - - private fun writeVarLong(value: Long) { - var v = value - while (v and -0x80L != 0L) { - stream.write((v and 0x7FL or 0x80L).toInt()) - v = v ushr 7 - } - stream.write(v.toInt()) - } - - fun addBuffer(id: Int, value: ByteArray) { - writeVarInt(id shl 3 or WireType.LENGTH_DELIMITED.value) - writeVarInt(value.size) - stream.write(value) - } - - fun addVarInt(id: Int, value: Int) = addVarInt(id, value.toLong()) - - fun addVarInt(id: Int, value: Long) { - writeVarInt(id shl 3) - writeVarLong(value) - } - - fun addString(id: Int, value: String) = addBuffer(id, value.toByteArray()) - - fun addFixed32(id: Int, value: Int) { - writeVarInt(id shl 3 or WireType.FIXED32.value) - val bytes = ByteArray(4) - for (i in 0..3) { - bytes[i] = (value shr (i * 8)).toByte() - } - stream.write(bytes) - } - - fun addFixed64(id: Int, value: Long) { - writeVarInt(id shl 3 or WireType.FIXED64.value) - val bytes = ByteArray(8) - for (i in 0..7) { - bytes[i] = (value shr (i * 8)).toByte() - } - stream.write(bytes) - } - - fun from(id: Int, writer: ProtoWriter.() -> Unit) { - val writerStream = ProtoWriter() - writer(writerStream) - addBuffer(id, writerStream.stream.toByteArray()) - } - - fun from(vararg ids: Int, writer: ProtoWriter.() -> Unit) { - val writerStream = ProtoWriter() - writer(writerStream) - var stream = writerStream.stream.toByteArray() - ids.reversed().forEach { id -> - with(ProtoWriter()) { - addBuffer(id, stream) - stream = this.stream.toByteArray() - } - } - stream.let(this.stream::write) - } - - fun addWire(wire: Wire) { - writeVarInt(wire.id shl 3 or wire.type.value) - when (wire.type) { - WireType.VARINT -> writeVarLong(wire.value as Long) - WireType.FIXED64, WireType.FIXED32 -> { - when (wire.value) { - is Int -> { - val bytes = ByteArray(4) - for (i in 0..3) { - bytes[i] = (wire.value shr (i * 8)).toByte() - } - stream.write(bytes) - } - is Long -> { - val bytes = ByteArray(8) - for (i in 0..7) { - bytes[i] = (wire.value shr (i * 8)).toByte() - } - stream.write(bytes) - } - is ByteArray -> stream.write(wire.value) - } - } - WireType.LENGTH_DELIMITED -> { - val value = wire.value as ByteArray - writeVarInt(value.size) - stream.write(value) - } - WireType.START_GROUP -> { - val value = wire.value as ByteArray - stream.write(value) - } - WireType.END_GROUP -> return - } - } - - fun toByteArray(): ByteArray { - return stream.toByteArray() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/WireType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/WireType.kt @@ -1,14 +0,0 @@ -package me.rhunk.snapenhance.util.protobuf; - -enum class WireType(val value: Int) { - VARINT(0), - FIXED64(1), - LENGTH_DELIMITED(2), - START_GROUP(3), - END_GROUP(4), - FIXED32(5); - - companion object { - fun fromValue(value: Int) = values().firstOrNull { it.value == value } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.util.snap - -object BitmojiSelfie { - enum class BitmojiSelfieType( - val prefixUrl: String, - ) { - STANDARD("https://sdk.bitmoji.com/render/panel/"), - THREE_D("https://images.bitmoji.com/3d/render/") - } - - fun getBitmojiSelfie(selfieId: String?, avatarId: String?, type: BitmojiSelfieType): String? { - if (selfieId.isNullOrEmpty() || avatarId.isNullOrEmpty()) { - return null - } - return when (type) { - BitmojiSelfieType.STANDARD -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?transparent=1" - BitmojiSelfieType.THREE_D -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?trim=circle" - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt @@ -1,49 +0,0 @@ -package me.rhunk.snapenhance.util.snap - -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.util.protobuf.ProtoReader -import java.io.InputStream -import java.util.Base64 -import javax.crypto.Cipher -import javax.crypto.CipherInputStream -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -object EncryptionHelper { - fun getEncryptionKeys(contentType: ContentType, messageProto: ProtoReader, isArroyo: Boolean): Pair<ByteArray, ByteArray>? { - val mediaEncryptionInfo = MediaDownloaderHelper.getMessageMediaEncryptionInfo(messageProto, contentType, isArroyo) ?: return null - val encryptionProtoIndex = if (mediaEncryptionInfo.contains(Constants.ENCRYPTION_PROTO_INDEX_V2)) { - Constants.ENCRYPTION_PROTO_INDEX_V2 - } else { - Constants.ENCRYPTION_PROTO_INDEX - } - val encryptionProto = mediaEncryptionInfo.followPath(encryptionProtoIndex) ?: return null - - var key: ByteArray = encryptionProto.getByteArray(1)!! - var iv: ByteArray = encryptionProto.getByteArray(2)!! - - if (encryptionProtoIndex == Constants.ENCRYPTION_PROTO_INDEX_V2) { - val decoder = Base64.getMimeDecoder() - key = decoder.decode(key) - iv = decoder.decode(iv) - } - - return Pair(key, iv) - } - - fun decryptInputStream( - inputStream: InputStream, - contentType: ContentType, - messageProto: ProtoReader, - isArroyo: Boolean - ): InputStream { - val encryptionKeys = getEncryptionKeys(contentType, messageProto, isArroyo) ?: throw Exception("Failed to get encryption keys") - - Cipher.getInstance("AES/CBC/PKCS5Padding").apply { - init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKeys.first, "AES"), IvParameterSpec(encryptionKeys.second)) - }.let { cipher -> - return CipherInputStream(inputStream, cipher) - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt @@ -1,70 +0,0 @@ -package me.rhunk.snapenhance.util.snap - -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.core.download.data.SplitMediaAssetType -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.util.download.RemoteMediaResolver -import me.rhunk.snapenhance.util.protobuf.ProtoReader -import java.io.ByteArrayInputStream -import java.io.FileNotFoundException -import java.io.InputStream -import java.util.zip.ZipInputStream - - -object MediaDownloaderHelper { - fun getMessageMediaEncryptionInfo(protoReader: ProtoReader, contentType: ContentType, isArroyo: Boolean): ProtoReader? { - val messageContainerPath = if (isArroyo) protoReader.followPath(*Constants.ARROYO_MEDIA_CONTAINER_PROTO_PATH)!! else protoReader - val mediaContainerPath = if (contentType == ContentType.NOTE) intArrayOf(6, 1, 1) else intArrayOf(5, 1, 1) - - return when (contentType) { - ContentType.NOTE -> messageContainerPath.followPath(*mediaContainerPath) - ContentType.SNAP -> messageContainerPath.followPath(*(intArrayOf(11) + mediaContainerPath)) - ContentType.EXTERNAL_MEDIA -> { - val externalMediaTypes = arrayOf( - intArrayOf(3, 3, *mediaContainerPath), //normal external media - intArrayOf(7, 15, 1, 1), //attached audio note - intArrayOf(7, 12, 3, *mediaContainerPath), //attached story reply - intArrayOf(7, 3, *mediaContainerPath), //original story reply - ) - externalMediaTypes.forEach { path -> - messageContainerPath.followPath(*path)?.also { return it } - } - null - } - else -> null - } - } - - fun downloadMediaFromReference( - mediaReference: ByteArray, - decryptionCallback: (InputStream) -> InputStream, - ): Map<SplitMediaAssetType, ByteArray> { - val inputStream = RemoteMediaResolver.downloadBoltMedia(mediaReference) ?: throw FileNotFoundException("Unable to get media key. Check the logs for more info") - val content = decryptionCallback(inputStream).readBytes() - val fileType = FileType.fromByteArray(content) - val isZipFile = fileType == FileType.ZIP - - //videos with overlay are packed in a zip file - //there are 2 files in the zip file, the video (webm) and the overlay (png) - if (isZipFile) { - var videoData: ByteArray? = null - var overlayData: ByteArray? = null - val zipInputStream = ZipInputStream(ByteArrayInputStream(content)) - while (zipInputStream.nextEntry != null) { - val zipEntryData: ByteArray = zipInputStream.readBytes() - val entryFileType = FileType.fromByteArray(zipEntryData) - if (entryFileType.isVideo) { - videoData = zipEntryData - } else if (entryFileType.isImage) { - overlayData = zipEntryData - } - } - videoData ?: throw FileNotFoundException("Unable to find video file in zip file") - overlayData ?: throw FileNotFoundException("Unable to find overlay file in zip file") - return mapOf(SplitMediaAssetType.ORIGINAL to videoData, SplitMediaAssetType.OVERLAY to overlayData) - } - - return mapOf(SplitMediaAssetType.ORIGINAL to content) - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt @@ -1,87 +0,0 @@ -package me.rhunk.snapenhance.util.snap - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import android.media.MediaDataSource -import android.media.MediaMetadataRetriever -import me.rhunk.snapenhance.data.FileType -import java.io.File - -object PreviewUtils { - fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? { - if (!isVideo) { - return BitmapFactory.decodeByteArray(data, 0, data.size) - } - return MediaMetadataRetriever().apply { - setDataSource(object : MediaDataSource() { - override fun readAt( - position: Long, - buffer: ByteArray, - offset: Int, - size: Int - ): Int { - var newSize = size - val length = data.size - if (position >= length) { - return -1 - } - if (position + newSize > length) { - newSize = length - position.toInt() - } - System.arraycopy(data, position.toInt(), buffer, offset, newSize) - return newSize - } - - override fun getSize(): Long { - return data.size.toLong() - } - override fun close() {} - }) - }.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) - } - - fun createPreviewFromFile(file: File): Bitmap? { - return if (FileType.fromFile(file).isVideo) { - MediaMetadataRetriever().apply { - setDataSource(file.absolutePath) - }.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) - } else { - BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options()) - } - } - - 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 mergeBitmapOverlay(originalMedia: Bitmap, overlayLayer: Bitmap): Bitmap { - val biggestBitmap = if (originalMedia.width * originalMedia.height > overlayLayer.width * overlayLayer.height) originalMedia else overlayLayer - val smallestBitmap = if (biggestBitmap == originalMedia) overlayLayer else originalMedia - - val mergedBitmap = Bitmap.createBitmap(biggestBitmap.width, biggestBitmap.height, biggestBitmap.config) - - with(Canvas(mergedBitmap)) { - val scaleMatrix = Matrix().apply { - postScale(biggestBitmap.width.toFloat() / smallestBitmap.width.toFloat(), biggestBitmap.height.toFloat() / smallestBitmap.height.toFloat()) - } - - if (biggestBitmap == originalMedia) { - drawBitmap(originalMedia, 0f, 0f, null) - drawBitmap(overlayLayer, scaleMatrix, null) - } else { - drawBitmap(originalMedia, scaleMatrix, null) - drawBitmap(overlayLayer, 0f, 0f, null) - } - } - - return mergedBitmap - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapWidgetBroadcastReceiverHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapWidgetBroadcastReceiverHelper.kt @@ -1,24 +0,0 @@ -package me.rhunk.snapenhance.util.snap - -import android.content.Intent -import me.rhunk.snapenhance.Constants - -object SnapWidgetBroadcastReceiverHelper { - private const val ACTION_WIDGET_UPDATE = "com.snap.android.WIDGET_APP_START_UPDATE_ACTION" - const val CLASS_NAME = "com.snap.widgets.core.BestFriendsWidgetProvider" - - fun create(targetAction: String, callback: Intent.() -> Unit): Intent { - with(Intent()) { - callback(this) - action = ACTION_WIDGET_UPDATE - putExtra(":)", true) - putExtra("action", targetAction) - setClassName(Constants.SNAPCHAT_PACKAGE_NAME, CLASS_NAME) - return this - } - } - - fun isIncomingIntentValid(intent: Intent): Boolean { - return intent.action == ACTION_WIDGET_UPDATE && intent.getBooleanExtra(":)", false) - } -}- \ No newline at end of file