commit 61da95f41b3716364a587a156ac8981b0c9ecf4b
parent 6b9e44700d15399dbfe24c76e3747518f752bca8
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 31 Aug 2023 00:59:30 +0200

feat: log system
- debug actions
- move packages to core

Diffstat:
Aapp/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 71+++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 35+++++++++++++++++------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt | 29+++++++++++++++--------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 7+++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt | 4+++-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/data/InstallationSummary.kt | 27++++++++++++++++++++++-----
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt | 148-------------------------------------------------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 2+-
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt | 15+++++++--------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt | 2--
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/ActivityLauncherHelper.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt | 2+-
Mcore/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl | 33+++++++++++++++------------------
Mcore/src/main/assets/lang/en_US.json | 6++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/Constants.kt | 2--
Mcore/src/main/kotlin/me/rhunk/snapenhance/Logger.kt | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt | 16+++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 9++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt | 9+--------
Acore/src/main/kotlin/me/rhunk/snapenhance/action/EnumAction.kt | 18++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt | 20--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt | 4++--
Dcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt | 11-----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt | 9++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt | 2+-
Dcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt | 12------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt | 145-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/FileLoaderWrapper.kt | 36------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt | 26--------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/types/FileActionType.kt | 6------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt | 101-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt | 166-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt | 71-----------------------------------------------------------------------
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/FileLoaderWrapper.kt | 36++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt | 26++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/FileActionType.kt | 6++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/LocaleWrapper.kt | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MappingsWrapper.kt | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/wrapper/MessageLoggerWrapper.kt | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt | 10++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt | 3++-
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseObject.kt | 7+++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/ConversationMessage.kt | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendFeedEntry.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/FriendInfo.kt | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/StoryEntry.kt | 27+++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/database/objects/UserConversationLink.kt | 23+++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadManagerClient.kt | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadTaskManager.kt | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMediaType.kt | 23+++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt | 10++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadObject.kt | 32++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadRequest.kt | 27+++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadStage.kt | 15+++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaEncryptionKeyPair.kt | 22++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt | 19+++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/core/download/data/SplitMediaAssetType.kt | 5+++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/EventBus.kt | 7+++----
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt | 254-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt | 7-------
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt | 50--------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedEntry.kt | 47-----------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt | 64----------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt | 27---------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt | 23-----------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt | 70----------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt | 183-------------------------------------------------------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMediaType.kt | 23-----------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt | 10----------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadObject.kt | 32--------------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt | 27---------------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadStage.kt | 15---------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt | 22----------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaFilter.kt | 19-------------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt | 5-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 32+++++++++++++++-----------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/ProfilePictureDownloader.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt | 7+++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt | 1-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt | 6+++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt | 45++++++++++++++++++++++-----------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt | 10++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt | 6++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt | 14+++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt | 9++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt | 2+-
107 files changed, 2486 insertions(+), 1883 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt @@ -0,0 +1,189 @@ +package me.rhunk.snapenhance + +import android.content.SharedPreferences +import android.util.Log +import java.io.File +import java.io.OutputStream +import java.io.RandomAccessFile +import java.time.format.DateTimeFormatter +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import kotlin.time.Duration.Companion.hours + +class LogLine( + val logLevel: LogLevel, + val dateTime: String, + val tag: String, + val message: String +) { + companion object { + fun fromString(line: String) = runCatching { + val parts = line.trimEnd().split("/") + if (parts.size != 4) return@runCatching null + LogLine( + LogLevel.fromLetter(parts[0]) ?: return@runCatching null, + parts[1], + parts[2], + parts[3] + ) + }.getOrNull() + } + + override fun toString(): String { + return "${logLevel.letter}/$dateTime/$tag/$message" + } +} + + +class LogReader( + logFile: File +) { + private val randomAccessFile = RandomAccessFile(logFile, "r") + private var startLineIndexes = mutableListOf<Long>() + var lineCount = queryLineCount() + + fun incrementLineCount() { + randomAccessFile.seek(randomAccessFile.length()) + startLineIndexes.add(randomAccessFile.filePointer) + lineCount++ + } + + private fun queryLineCount(): Int { + randomAccessFile.seek(0) + var lines = 0 + var lastIndex: Long + while (true) { + lastIndex = randomAccessFile.filePointer + randomAccessFile.readLine() ?: break + startLineIndexes.add(lastIndex) + lines++ + } + return lines + } + + private fun getLine(index: Int): String? { + if (index <= 0 || index > lineCount) return null + randomAccessFile.seek(startLineIndexes[index]) + return randomAccessFile.readLine() + } + + fun getLogLine(index: Int): LogLine? { + return getLine(index)?.let { LogLine.fromString(it) } + } +} + + +class LogManager( + remoteSideContext: RemoteSideContext +) { + companion object { + private const val TAG = "SnapEnhanceManager" + private val LOG_LIFETIME = 24.hours + } + + var lineAddListener = { _: LogLine -> } + + private val logFolder = File(remoteSideContext.androidContext.cacheDir, "logs") + private val preferences: SharedPreferences + + private var logFile: File + + init { + if (!logFolder.exists()) { + logFolder.mkdirs() + } + preferences = remoteSideContext.androidContext.getSharedPreferences("logger", 0) + logFile = preferences.getString("log_file", null)?.let { File(it) }?.takeIf { it.exists() } ?: run { + newLogFile() + logFile + } + + if (System.currentTimeMillis() - preferences.getLong("last_created", 0) > LOG_LIFETIME.inWholeMilliseconds) { + newLogFile() + } + } + + private fun getCurrentDateTime(pathSafe: Boolean = false): String { + return DateTimeFormatter.ofPattern(if (pathSafe) "yyyy-MM-dd_HH-mm-ss" else "yyyy-MM-dd HH:mm:ss").format( + java.time.LocalDateTime.now() + ) + } + + private fun newLogFile() { + val currentTime = System.currentTimeMillis() + logFile = File(logFolder, "snapenhance_${getCurrentDateTime(pathSafe = true)}.log").also { + it.createNewFile() + } + preferences.edit().putString("log_file", logFile.absolutePath).putLong("last_created", currentTime).apply() + } + + fun clearLogs() { + logFile.delete() + newLogFile() + } + + fun getLogFile() = logFile + + fun exportLogsToZip(outputStream: OutputStream) { + val zipOutputStream = ZipOutputStream(outputStream) + //add logFolder to zip + logFolder.walk().forEach { + if (it.isFile) { + zipOutputStream.putNextEntry(ZipEntry(it.name)) + it.inputStream().copyTo(zipOutputStream) + zipOutputStream.closeEntry() + } + } + + //add device info to zip + zipOutputStream.putNextEntry(ZipEntry("device_info.txt")) + + + zipOutputStream.close() + } + + fun newReader(onAddLine: (LogLine) -> Unit) = LogReader(logFile).also { + lineAddListener = { line -> it.incrementLineCount(); onAddLine(line) } + } + + 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) + } + + 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) + } + + fun internalLog(tag: String, logLevel: LogLevel, message: Any?) { + runCatching { + val line = LogLine(logLevel, getCurrentDateTime(), tag, message.toString()) + logFile.appendText("$line\n", Charsets.UTF_8) + lineAddListener(line) + Log.println(logLevel.priority, tag, message.toString()) + }.onFailure { + Log.println(Log.ERROR, tag, "Failed to log message: $message") + Log.println(Log.ERROR, tag, it.toString()) + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -3,9 +3,12 @@ package me.rhunk.snapenhance import android.app.Activity import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.core.app.CoreComponentFactory import androidx.documentfile.provider.DocumentFile import coil.ImageLoader import coil.decode.VideoFrameDecoder @@ -13,18 +16,25 @@ import coil.disk.DiskCache import coil.memory.MemoryCache import kotlinx.coroutines.Dispatchers import me.rhunk.snapenhance.bridge.BridgeService -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper -import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper +import me.rhunk.snapenhance.core.BuildConfig +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.download.DownloadTaskManager +import me.rhunk.snapenhance.core.download.DownloadTaskManager import me.rhunk.snapenhance.messaging.ModDatabase import me.rhunk.snapenhance.messaging.StreaksReminder +import me.rhunk.snapenhance.ui.manager.MainActivity import me.rhunk.snapenhance.ui.manager.data.InstallationSummary -import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo +import me.rhunk.snapenhance.ui.manager.data.ModInfo +import me.rhunk.snapenhance.ui.manager.data.PlatformInfo import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo import me.rhunk.snapenhance.ui.setup.Requirements import me.rhunk.snapenhance.ui.setup.SetupActivity +import java.io.ByteArrayInputStream import java.lang.ref.WeakReference +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + class RemoteSideContext( val androidContext: Context @@ -42,6 +52,7 @@ class RemoteSideContext( val downloadTaskManager = DownloadTaskManager() val modDatabase = ModDatabase(this) val streaksReminder = StreaksReminder(this) + val log = LogManager(this) //used to load bitmoji selfies and download previews val imageLoader by lazy { @@ -76,37 +87,57 @@ class RemoteSideContext( modDatabase.init() streaksReminder.init() }.onFailure { - Logger.error("Failed to load RemoteSideContext", it) + log.error("Failed to load RemoteSideContext", it) } } - fun getInstallationSummary() = InstallationSummary( - snapchatInfo = mappings.getSnapchatPackageInfo()?.let { - SnapchatAppInfo( - version = it.versionName, - versionCode = it.longVersionCode - ) - }, - mappingsInfo = if (mappings.isMappingsLoaded()) { - ModMappingsInfo( - generatedSnapchatVersion = mappings.getGeneratedBuildNumber(), - isOutdated = mappings.isMappingsOutdated() + val installationSummary by lazy { + InstallationSummary( + snapchatInfo = mappings.getSnapchatPackageInfo()?.let { + SnapchatAppInfo( + packageName = it.packageName, + version = it.versionName, + versionCode = it.longVersionCode, + isLSPatched = it.applicationInfo.appComponentFactory != CoreComponentFactory::class.java.name, + isSplitApk = it.splitNames.isNotEmpty() + ) + }, + modInfo = ModInfo( + loaderPackageName = MainActivity::class.java.`package`?.name ?: "unknown", + buildPackageName = BuildConfig.APPLICATION_ID, + buildVersion = BuildConfig.VERSION_NAME, + buildVersionCode = BuildConfig.VERSION_CODE.toLong(), + buildIssuer = androidContext.packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_SIGNING_CERTIFICATES) + ?.signingInfo?.apkContentsSigners?.firstOrNull()?.let { + val certFactory = CertificateFactory.getInstance("X509") + val cert = certFactory.generateCertificate(ByteArrayInputStream(it.toByteArray())) as X509Certificate + cert.issuerDN.toString() + } ?: throw Exception("Failed to get certificate info"), + isDebugBuild = BuildConfig.DEBUG, + mappingVersion = mappings.getGeneratedBuildNumber(), + mappingsOutdated = mappings.isMappingsOutdated() + ), + platformInfo = PlatformInfo( + device = Build.DEVICE, + buildFingerprint = Build.FINGERPRINT, + androidVersion = Build.VERSION.RELEASE, + systemAbi = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown" ) - } else null - ) + ) + } fun longToast(message: Any) { androidContext.mainExecutor.execute { Toast.makeText(androidContext, message.toString(), Toast.LENGTH_LONG).show() } - Logger.debug(message.toString()) + log.debug(message.toString()) } fun shortToast(message: Any) { androidContext.mainExecutor.execute { Toast.makeText(androidContext, message.toString(), Toast.LENGTH_SHORT).show() } - Logger.debug(message.toString()) + log.debug(message.toString()) } fun checkForRequirements(overrideRequirements: Int? = null): Boolean { 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,16 +3,16 @@ package me.rhunk.snapenhance.bridge import android.app.Service import android.content.Intent import android.os.IBinder -import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.LogLevel import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.bridge.types.FileActionType -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper -import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.bridge.types.FileActionType +import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.bridge.wrapper.MessageLoggerWrapper +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.database.objects.FriendInfo import me.rhunk.snapenhance.download.DownloadProcessor import me.rhunk.snapenhance.util.SerializableDataObject import kotlin.system.measureTimeMillis @@ -36,7 +36,7 @@ class BridgeService : Service() { fun triggerFriendSync(friendId: String) { val syncedFriend = syncCallback.syncFriend(friendId) if (syncedFriend == null) { - Logger.error("Failed to sync friend $friendId") + remoteSideContext.log.error("Failed to sync friend $friendId") return } SerializableDataObject.fromJson<FriendInfo>(syncedFriend).let { @@ -47,7 +47,7 @@ class BridgeService : Service() { fun triggerGroupSync(groupId: String) { val syncedGroup = syncCallback.syncGroup(groupId) if (syncedGroup == null) { - Logger.error("Failed to sync group $groupId") + remoteSideContext.log.error("Failed to sync group $groupId") return } SerializableDataObject.fromJson<MessagingGroupInfo>(syncedGroup).let { @@ -56,10 +56,12 @@ class BridgeService : Service() { } inner class BridgeBinder : BridgeInterface.Stub() { + override fun broadcastLog(tag: String, level: String, message: String) { + remoteSideContext.log.internalLog(tag, LogLevel.fromShortName(level) ?: LogLevel.INFO, message) + } + override fun fileOperation(action: Int, fileType: Int, content: ByteArray?): ByteArray { - val resolvedFile by lazy { - BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) - } + val resolvedFile = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) return when (FileActionType.values()[action]) { FileActionType.CREATE_AND_READ -> { @@ -108,8 +110,6 @@ class BridgeService : Service() { override fun deleteMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.deleteMessage(conversationId, id) - override fun clearMessageLogger() = messageLoggerWrapper.clearMessages() - override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir override fun fetchLocales(userLocale: String) = @@ -133,25 +133,24 @@ class BridgeService : Service() { } override fun sync(callback: SyncCallback) { - Logger.debug("Syncing remote") syncCallback = callback measureTimeMillis { remoteSideContext.modDatabase.getFriends().map { it.userId } .forEach { friendId -> runCatching { triggerFriendSync(friendId) }.onFailure { - Logger.error("Failed to sync friend $friendId", it) + remoteSideContext.log.error("Failed to sync friend $friendId", it) } } remoteSideContext.modDatabase.getGroups().map { it.conversationId }.forEach { groupId -> runCatching { triggerGroupSync(groupId) }.onFailure { - Logger.error("Failed to sync group $groupId", it) + remoteSideContext.log.error("Failed to sync group $groupId", it) } } }.also { - Logger.debug("Syncing remote took $it ms") + remoteSideContext.log.verbose("Syncing remote took $it ms") } } @@ -159,7 +158,7 @@ class BridgeService : Service() { groups: List<String>, friends: List<String> ) { - Logger.debug("Received ${groups.size} groups and ${friends.size} friends") + remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends") remoteSideContext.modDatabase.receiveMessagingDataCallback( friends.map { SerializableDataObject.fromJson<MessagingFriendInfo>(it) }, groups.map { SerializableDataObject.fromJson<MessagingGroupInfo>(it) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -19,14 +19,15 @@ import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.core.download.DownloadManagerClient import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.download.data.DownloadMediaType -import me.rhunk.snapenhance.download.data.DownloadMetadata -import me.rhunk.snapenhance.download.data.DownloadObject -import me.rhunk.snapenhance.download.data.DownloadRequest -import me.rhunk.snapenhance.download.data.DownloadStage -import me.rhunk.snapenhance.download.data.InputMedia -import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair +import me.rhunk.snapenhance.core.download.data.DownloadMediaType +import me.rhunk.snapenhance.core.download.data.DownloadMetadata +import me.rhunk.snapenhance.core.download.data.DownloadObject +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.util.snap.MediaDownloaderHelper import java.io.File @@ -178,14 +179,14 @@ class DownloadProcessor ( mediaScanIntent.setData(outputFile.uri) remoteSideContext.androidContext.sendBroadcast(mediaScanIntent) }.onFailure { - Logger.error("Failed to scan media file", it) + remoteSideContext.log.error("Failed to scan media file", it) callbackOnFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message) } - Logger.debug("download complete") + remoteSideContext.log.verbose("download complete") callbackOnSuccess(fileName) }.onFailure { exception -> - Logger.error(exception) + remoteSideContext.log.error("Failed to save media to gallery", exception) callbackOnFailure(translation.format("failed_gallery_toast", "error" to exception.toString()), exception.message) downloadObject.downloadStage = DownloadStage.FAILED } @@ -284,7 +285,7 @@ class DownloadProcessor ( saveMediaToGallery(outputFile, downloadObjectObject) }.onFailure { exception -> if (coroutineContext.job.isCancelled) return@onFailure - Logger.error(exception) + remoteSideContext.log.error("Failed to download dash media", exception) callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message) downloadObjectObject.downloadStage = DownloadStage.FAILED } @@ -333,7 +334,7 @@ class DownloadProcessor ( val downloadedMedias = downloadInputMedias(downloadRequest).map { it.key to DownloadedFile(it.value, FileType.fromFile(it.value)) }.toMap().toMutableMap() - Logger.debug("downloaded ${downloadedMedias.size} medias") + remoteSideContext.log.verbose("downloaded ${downloadedMedias.size} medias") var shouldMergeOverlay = downloadRequest.shouldMergeOverlay @@ -376,7 +377,7 @@ class DownloadProcessor ( saveMediaToGallery(mergedOverlay, downloadObjectObject) }.onFailure { exception -> if (coroutineContext.job.isCancelled) return@onFailure - Logger.error(exception) + remoteSideContext.log.error("Failed to merge overlay", exception) callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message) downloadObjectObject.downloadStage = DownloadStage.MERGE_FAILED } @@ -390,7 +391,7 @@ class DownloadProcessor ( downloadRemoteMedia(downloadObjectObject, downloadedMedias, downloadRequest) }.onFailure { exception -> downloadObjectObject.downloadStage = DownloadStage.FAILED - Logger.error(exception) + remoteSideContext.log.error("Failed to download media", exception) callbackOnFailure(translation["failed_generic_toast"], exception.message) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -1,13 +1,12 @@ package me.rhunk.snapenhance.messaging import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.RemoteSideContext +import me.rhunk.snapenhance.core.database.objects.FriendInfo 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.database.objects.FriendInfo import me.rhunk.snapenhance.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.util.ktx.getInteger import me.rhunk.snapenhance.util.ktx.getLongOrNull @@ -28,7 +27,7 @@ class ModDatabase( runCatching { block() }.onFailure { - Logger.error("Failed to execute async block", it) + context.log.error("Failed to execute async block", it) } } } @@ -103,7 +102,7 @@ class ModDatabase( selfieId = cursor.getStringOrNull("selfieId") )) }.onFailure { - Logger.error("Failed to parse friend", it) + context.log.error("Failed to parse friend", it) } } friends diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -13,7 +13,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.ui.manager.sections.HomeSection +import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection import me.rhunk.snapenhance.ui.manager.sections.NotImplemented import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection @@ -64,6 +64,8 @@ open class Section { lateinit var context: RemoteSideContext lateinit var navController: NavController + val currentRoute get() = navController.currentBackStackEntry?.destination?.route + open fun init() {} open fun onResumed() {} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/data/InstallationSummary.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/data/InstallationSummary.kt @@ -2,16 +2,33 @@ package me.rhunk.snapenhance.ui.manager.data data class SnapchatAppInfo( + val packageName: String, val version: String, - val versionCode: Long + val versionCode: Long, + val isLSPatched: Boolean, + val isSplitApk: Boolean, ) -data class ModMappingsInfo( - val generatedSnapchatVersion: Long, - val isOutdated: Boolean +data class ModInfo( + val loaderPackageName: String, + val buildPackageName: String, + val buildVersion: String, + val buildVersionCode: Long, + val buildIssuer: String, + val isDebugBuild: Boolean, + val mappingVersion: Long?, + val mappingsOutdated: Boolean?, +) + +data class PlatformInfo( + val device: String, + val buildFingerprint: String, + val androidVersion: String, + val systemAbi: String, ) data class InstallationSummary( + val platformInfo: PlatformInfo, val snapchatInfo: SnapchatAppInfo?, - val mappingsInfo: ModMappingsInfo? + val modInfo: ModInfo?, ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt @@ -1,147 +0,0 @@ -package me.rhunk.snapenhance.ui.manager.sections - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Language -import androidx.compose.material.icons.filled.Map -import androidx.compose.material.icons.filled.OpenInNew -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import me.rhunk.snapenhance.ui.manager.Section -import me.rhunk.snapenhance.ui.manager.data.InstallationSummary -import me.rhunk.snapenhance.ui.setup.Requirements -import java.util.Locale - -class HomeSection : Section() { - companion object { - val cardMargin = 10.dp - } - private val installationSummary = mutableStateOf(null as InstallationSummary?) - private val userLocale = mutableStateOf(null as String?) - - @Composable - private fun SummaryCards(installationSummary: InstallationSummary) { - //installation summary - OutlinedCard( - modifier = Modifier - .padding(all = cardMargin) - .fillMaxWidth() - ) { - Column(modifier = Modifier.padding(all = 16.dp)) { - if (installationSummary.snapchatInfo != null) { - Text("Snapchat version: ${installationSummary.snapchatInfo.version}") - Text("Snapchat version code: ${installationSummary.snapchatInfo.versionCode}") - } else { - Text("Snapchat not installed/detected") - } - } - } - - OutlinedCard( - modifier = Modifier - .padding(all = cardMargin) - .fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(all = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Icon( - Icons.Filled.Map, - contentDescription = "Mappings", - modifier = Modifier - .padding(end = 10.dp) - .align(Alignment.CenterVertically) - ) - - Text(text = if (installationSummary.mappingsInfo == null || installationSummary.mappingsInfo.isOutdated) { - "Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}" - } else { - "Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}" - }, modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) - - //inline button - Button(onClick = { - context.checkForRequirements(Requirements.MAPPINGS) - }, modifier = Modifier.height(40.dp)) { - Icon(Icons.Filled.Refresh, contentDescription = "Refresh") - } - } - } - OutlinedCard( - modifier = Modifier - .padding(all = cardMargin) - .fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(all = 16.dp), - ) { - Icon( - Icons.Filled.Language, - contentDescription = "Language", - modifier = Modifier - .padding(end = 10.dp) - .align(Alignment.CenterVertically) - ) - Text(text = userLocale.value ?: "Unknown", modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) - - //inline button - Button(onClick = { - context.checkForRequirements(Requirements.LANGUAGE) - }, modifier = Modifier.height(40.dp)) { - Icon(Icons.Filled.OpenInNew, contentDescription = null) - } - } - } - } - - override fun onResumed() { - if (!context.mappings.isMappingsLoaded()) { - context.mappings.init(context.androidContext) - } - installationSummary.value = context.getInstallationSummary() - userLocale.value = context.translation.loadedLocale.getDisplayName(Locale.getDefault()) - } - - override fun sectionTopBarName() = "SnapEnhance" - - @Composable - @Preview - override fun Content() { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(ScrollState(0)) - ) { - Text( - text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl.", - modifier = Modifier.padding(16.dp) - ) - - SummaryCards(installationSummary = installationSummary.value ?: return) - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt @@ -51,8 +51,8 @@ import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter import kotlinx.coroutines.launch import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.download.data.DownloadObject -import me.rhunk.snapenhance.download.data.MediaFilter +import me.rhunk.snapenhance.core.download.data.DownloadObject +import me.rhunk.snapenhance.core.download.data.MediaFilter import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.BitmojiImage import me.rhunk.snapenhance.ui.util.ImageRequestHelper diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -439,7 +439,7 @@ class FeaturesSection : Section() { IconButton(onClick = { showSearchBar = showSearchBar.not() - if (!showSearchBar && navController.currentBackStackEntry?.destination?.route == SEARCH_FEATURE_ROUTE) { + if (!showSearchBar && currentRoute == SEARCH_FEATURE_ROUTE) { navController.navigate(MAIN_ROUTE) } }) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -0,0 +1,312 @@ +package me.rhunk.snapenhance.ui.manager.sections.home + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Map +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.material.icons.filled.ReceiptLong +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.manager.data.InstallationSummary +import me.rhunk.snapenhance.ui.setup.Requirements +import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper +import me.rhunk.snapenhance.ui.util.saveFile +import java.util.Locale + +class HomeSection : Section() { + companion object { + val cardMargin = 10.dp + const val HOME_ROOT = "home_root" + const val DEBUG_SECTION_ROUTE = "home_debug" + const val LOGS_SECTION_ROUTE = "home_logs" + } + + private val installationSummary = mutableStateOf(null as InstallationSummary?) + private val userLocale = mutableStateOf(null as String?) + private val homeSubSection by lazy { HomeSubSection(context) } + private lateinit var activityLauncherHelper: ActivityLauncherHelper + + override fun init() { + activityLauncherHelper = ActivityLauncherHelper(context.activity!!) + } + + @Composable + private fun SummaryCardRow(icon: ImageVector? = null, title: String, action: @Composable () -> Unit) { + Row( + modifier = Modifier.padding(all = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + icon?.let { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier + .padding(end = 10.dp) + .align(Alignment.CenterVertically) + ) + } + Text(text = title, modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) + Column { + action() + } + } + } + + @Composable + private fun SummaryCards(installationSummary: InstallationSummary) { + OutlinedCard( + modifier = Modifier + .padding(all = cardMargin) + .fillMaxWidth() + ) { + SummaryCardRow( + icon = Icons.Filled.Map, + title = if (installationSummary.modInfo == null || installationSummary.modInfo.mappingsOutdated == true) { + "Mappings ${if (installationSummary.modInfo == null) "not generated" else "outdated"}" + } else { + "Mappings version ${installationSummary.modInfo.mappingVersion}" + } + ) { + Button(onClick = { + context.checkForRequirements(Requirements.MAPPINGS) + }, modifier = Modifier.height(40.dp)) { + Icon(Icons.Filled.Refresh, contentDescription = null) + } + } + + SummaryCardRow(icon = Icons.Filled.Language, title = userLocale.value ?: "Unknown") { + Button(onClick = { + context.checkForRequirements(Requirements.LANGUAGE) + }, modifier = Modifier.height(40.dp)) { + Icon(Icons.Filled.OpenInNew, contentDescription = null) + } + } + } + + val summaryInfo = remember { + mapOf( + "Build Issuer" to (installationSummary.modInfo?.buildIssuer ?: "Unknown"), + "Device" to installationSummary.platformInfo.device, + "Android version" to installationSummary.platformInfo.androidVersion, + "System ABI" to installationSummary.platformInfo.systemAbi, + "Build fingerprint" to installationSummary.platformInfo.buildFingerprint + ) + } + + Card( + modifier = Modifier + .padding(all = cardMargin) + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + ) { + summaryInfo.forEach { (title, value) -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 5.dp), + ) { + Text( + text = title, + fontSize = 12.sp, + fontWeight = FontWeight.Light, + ) + Text( + fontSize = 14.sp, + text = value, + lineHeight = 20.sp + ) + } + } + } + + } + } + + override fun onResumed() { + if (!context.mappings.isMappingsLoaded()) { + context.mappings.init(context.androidContext) + } + installationSummary.value = context.installationSummary + userLocale.value = context.translation.loadedLocale.getDisplayName(Locale.getDefault()) + } + + override fun sectionTopBarName(): String { + if (currentRoute == HOME_ROOT) { + return "" + } + return context.translation["manager.routes.$currentRoute"] + } + + @Composable + override fun FloatingActionButton() { + if (currentRoute == LOGS_SECTION_ROUTE) { + homeSubSection.LogsActionButtons() + } + } + + @Composable + override fun TopBarActions(rowScope: RowScope) { + rowScope.apply { + when (currentRoute) { + HOME_ROOT -> { + IconButton(onClick = { + navController.navigate(LOGS_SECTION_ROUTE) + }) { + Icon(Icons.Filled.ReceiptLong, contentDescription = null) + } + IconButton(onClick = { + navController.navigate(DEBUG_SECTION_ROUTE) + }) { + Icon(Icons.Filled.BugReport, contentDescription = null) + } + } + LOGS_SECTION_ROUTE -> { + var showDropDown by remember { mutableStateOf(false) } + + IconButton(onClick = { + showDropDown = true + }) { + Icon(Icons.Filled.MoreVert, contentDescription = null) + } + + DropdownMenu( + expanded = showDropDown, + onDismissRequest = { showDropDown = false }, + modifier = Modifier.align(Alignment.CenterVertically) + ) { + DropdownMenuItem(onClick = { + context.log.clearLogs() + navController.navigate(LOGS_SECTION_ROUTE) + showDropDown = false + }, text = { + Text(text = "Clear logs") + }) + + DropdownMenuItem(onClick = { + val logFile = context.log.getLogFile() + activityLauncherHelper.saveFile(logFile.name, "text/plain") { uri -> + context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use { + logFile.inputStream().copyTo(it) + context.longToast("Saved logs to $uri") + } + } + showDropDown = false + }, text = { + Text(text = "Export logs") + }) + } + } + } + } + } + + override fun build(navGraphBuilder: NavGraphBuilder) { + navGraphBuilder.navigation( + route = enumSection.route, + startDestination = HOME_ROOT + ) { + composable(HOME_ROOT) { + Content() + } + composable(LOGS_SECTION_ROUTE) { + homeSubSection.LogsSection() + } + composable(DEBUG_SECTION_ROUTE) { + homeSubSection.DebugSection() + } + } + } + + + @Composable + @Preview + override fun Content() { + Column( + modifier = Modifier + .verticalScroll(ScrollState(0)) + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.launcher_icon_monochrome), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + contentScale = ContentScale.FillHeight, + modifier = Modifier + .height(120.dp) + .scale(1.75f) + ) + Text( + text = ("\u0065" + "\u0063" + "\u006e" + "\u0061" + "\u0068" + "\u006e" + "\u0045" + "\u0070" + "\u0061" + "\u006e" + "\u0053").reversed(), + fontSize = 30.sp, + modifier = Modifier.padding(16.dp), + ) + } + + + Text( + text = "An xposed module that enhances the Snapchat experience", + modifier = Modifier.padding(16.dp) + ) + + SummaryCards(installationSummary = installationSummary.value ?: return) + } + } +}+ \ No newline at end of file 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 @@ -0,0 +1,214 @@ +package me.rhunk.snapenhance.ui.manager.sections.home + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown +import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp +import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.LogReader +import me.rhunk.snapenhance.RemoteSideContext +import me.rhunk.snapenhance.action.EnumAction +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType +import me.rhunk.snapenhance.manager.impl.ActionManager +import me.rhunk.snapenhance.ui.util.AlertDialogs + +class HomeSubSection( + private val context: RemoteSideContext +) { + private val dialogs by lazy { AlertDialogs(context.translation) } + + private lateinit var logListState: LazyListState + + @Composable + private fun RowAction(title: String, requireConfirmation: Boolean = false, action: () -> Unit) { + var confirmationDialog by remember { + mutableStateOf(false) + } + + fun takeAction() { + if (requireConfirmation) { + confirmationDialog = true + } else { + action() + } + } + + if (requireConfirmation && confirmationDialog) { + Dialog(onDismissRequest = { confirmationDialog = false }) { + dialogs.ConfirmDialog(title = "Are you sure?", onConfirm = { + action() + confirmationDialog = false + }, onDismiss = { + confirmationDialog = false + }) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .height(65.dp) + .clickable { + takeAction() + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = title, modifier = Modifier.padding(start = 26.dp)) + IconButton(onClick = { takeAction() }) { + Icon( + imageVector = Icons.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } + } + + @Composable + fun LogsSection() { + val coroutineScope = rememberCoroutineScope() + var lineCount by remember { mutableIntStateOf(0) } + var logReader by remember { mutableStateOf<LogReader?>(null) } + logListState = remember { LazyListState(0) } + + Column( + modifier = Modifier + .fillMaxSize() + ) { + LazyColumn( + modifier = Modifier.background(MaterialTheme.colorScheme.surfaceVariant), + state = logListState + ) { + items(lineCount) { index -> + val line = logReader?.getLogLine(index) ?: return@items + Box(modifier = Modifier + .fillMaxWidth() + .background( + if (index % 2 == 0) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceVariant + )) { + Text(text = line.message, modifier = Modifier.padding(9.dp), fontSize = 10.sp) + } + } + } + + if (logReader == null) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) + } + + LaunchedEffect(Unit) { + coroutineScope.launch(Dispatchers.IO) { + runCatching { + logReader = context.log.newReader { + lineCount++ + } + lineCount = logReader!!.lineCount + }.onFailure { + context.longToast("Failed to read logs!") + } + } + } + } + } + + @Composable + fun LogsActionButtons() { + val coroutineScope = rememberCoroutineScope() + Column( + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + FilledIconButton(onClick = { + coroutineScope.launch { + logListState.scrollToItem(0) + } + }) { + Icon(Icons.Filled.KeyboardDoubleArrowUp, contentDescription = null) + } + + FilledIconButton(onClick = { + coroutineScope.launch { + logListState.scrollToItem(logListState.layoutInfo.totalItemsCount - 1) + } + }) { + Icon(Icons.Filled.KeyboardDoubleArrowDown, contentDescription = null) + } + } + } + + private fun launchActionIntent(action: EnumAction) { + val intent = context.androidContext.packageManager.getLaunchIntentForPackage(Constants.SNAPCHAT_PACKAGE_NAME) + intent?.putExtra(ActionManager.ACTION_PARAMETER, action.key) + context.androidContext.startActivity(intent) + } + + @Composable + private fun RowTitle(title: String) { + Text(text = title, modifier = Modifier.padding(16.dp), fontSize = 20.sp, fontWeight = FontWeight.Bold) + } + + @Composable + fun DebugSection() { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(ScrollState(0)) + ) { + RowTitle(title = "Actions") + EnumAction.values().forEach { enumAction -> + RowAction(title = context.translation["actions.${enumAction.key}"]) { + launchActionIntent(enumAction) + } + } + + RowTitle(title = "Clear Files") + BridgeFileType.values().forEach { fileType -> + RowAction(title = fileType.displayName, requireConfirmation = true) { + runCatching { + fileType.resolve(context.androidContext).delete() + context.longToast("Deleted ${fileType.displayName}!") + }.onFailure { + context.longToast("Failed to delete ${fileType.displayName}!") + } + } + } + } + } +}+ \ No newline at end of file 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 @@ -43,9 +43,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.bridge.BridgeClient +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 @@ -55,8 +54,8 @@ class AddFriendDialog( private val section: SocialSection, ) { @Composable - private fun ListCardEntry(name: String, currentState: () -> Boolean, onState: (Boolean) -> Unit = {}) { - var currentState by remember { mutableStateOf(currentState()) } + private fun ListCardEntry(name: String, getCurrentState: () -> Boolean, onState: (Boolean) -> Unit = {}) { + var currentState by remember { mutableStateOf(getCurrentState()) } Row( modifier = Modifier @@ -74,7 +73,7 @@ class AddFriendDialog( modifier = Modifier .weight(1f) .onGloballyPositioned { - currentState = currentState() + currentState = getCurrentState() } ) @@ -149,7 +148,7 @@ class AddFriendDialog( runCatching { context.androidContext.sendBroadcast(it) }.onFailure { - Logger.error("Failed to send broadcast", it) + context.log.error("Failed to send broadcast", it) hasFetchError = true } } @@ -234,7 +233,7 @@ class AddFriendDialog( val group = filteredGroups[it] ListCardEntry( name = group.name, - currentState = { context.modDatabase.getGroupInfo(group.conversationId) != null } + getCurrentState = { context.modDatabase.getGroupInfo(group.conversationId) != null } ) { state -> if (state) { context.bridgeService.triggerGroupSync(group.conversationId) @@ -261,7 +260,7 @@ class AddFriendDialog( ListCardEntry( name = friend.displayName?.takeIf { name -> name.isNotBlank() } ?: friend.mutableUsername, - currentState = { context.modDatabase.getFriendInfo(friend.userId) != null } + getCurrentState = { context.modDatabase.getFriendInfo(friend.userId) != null } ) { state -> if (state) { context.bridgeService.triggerFriendSync(friend.userId) 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 @@ -79,7 +79,7 @@ class SocialSection : Section() { groupList = context.modDatabase.getGroups() } - override fun canGoBack() = navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE + override fun canGoBack() = currentRoute != MAIN_ROUTE override fun build(navGraphBuilder: NavGraphBuilder) { navGraphBuilder.navigation(route = enumSection.route, startDestination = MAIN_ROUTE) { @@ -117,7 +117,7 @@ class SocialSection : Section() { } } - if (navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE) { + if (currentRoute != MAIN_ROUTE) { IconButton( onClick = { deleteConfirmDialog = true }, ) { 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 @@ -44,7 +44,7 @@ class MappingsScreen : SetupScreen() { fun tryToGenerateMappings() { //check for snapchat installation - val installationSummary = context.getInstallationSummary() + val installationSummary = context.installationSummary if (installationSummary.snapchatInfo == null) { throw Exception(context.translation["setup.mappings.generate_failure_no_snapchat"]) } @@ -69,7 +69,7 @@ class MappingsScreen : SetupScreen() { }.onFailure { isGenerating = false infoText = context.translation["setup.mappings.generate_failure"] + "\n\n" + it.message - Logger.error("Failed to generate mappings", it) + context.log.error("Failed to generate mappings", it) } } }) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.util.ObservableMutableState import java.util.Locale diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.ObservableMutableState @@ -23,7 +22,6 @@ class SaveFolderScreen : SetupScreen() { saveFolder = ObservableMutableState( defaultValue = "", onChange = { _, newValue -> - Logger.debug(newValue) if (newValue.isNotBlank()) { context.config.root.downloader.saveFolder.set(newValue) context.config.writeConfig() 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 @@ -16,7 +16,7 @@ class ActivityLauncherHelper( runCatching { callback?.let { it(result.data!!) } }.onFailure { - Logger.error("Failed to process activity result", it) + Logger.directError("Failed to process activity result", it) } } callback = null diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.config.DataProcessors import me.rhunk.snapenhance.core.config.PropertyPair diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -6,52 +6,46 @@ import me.rhunk.snapenhance.bridge.SyncCallback; interface BridgeInterface { /** + * broadcast a log message + */ + void broadcastLog(String tag, String level, String message); + + /** * Execute a file operation + * @param fileType the corresponding file type (see BridgeFileType) */ byte[] fileOperation(int action, int fileType, in @nullable byte[] content); /** * Get the content of a logged message from the database - * - * @param conversationId the ID of the conversation - * @return the content of the message + * @return message ids that are logged */ long[] getLoggedMessageIds(String conversationId, int limit); /** * Get the content of a logged message from the database - * - * @param id the ID of the message logger message - * @return the content of the message */ @nullable byte[] getMessageLoggerMessage(String conversationId, long id); /** * Add a message to the message logger database - * - * @param id the ID of the message logger message - * @param message the content of the message */ void addMessageLoggerMessage(String conversationId, long id, in byte[] message); /** * Delete a message from the message logger database - * - * @param id the ID of the message logger message */ void deleteMessageLoggerMessage(String conversationId, long id); /** - * Clear the message logger database - */ - void clearMessageLogger(); - + * Get the application APK path (assets for the conversation exporter) + */ String getApplicationApkPath(); /** * Fetch the locales * - * @return the locale result + * @return the map of locales (key: locale short name, value: locale data as json) */ Map<String, String> fetchLocales(String userLocale); @@ -62,11 +56,14 @@ interface BridgeInterface { /** * Get rules for a given user or conversation + * @return list of rules (MessagingRuleType) */ List<String> getRules(String uuid); /** * Update rule for a giver user or conversation + * + * @param type rule type (MessagingRuleType) */ void setRule(String uuid, String type, boolean state); @@ -77,8 +74,8 @@ interface BridgeInterface { /** * Pass all groups and friends to be able to add them to the database - * @param groups serialized groups - * @param friends serialized friends + * @param groups list of groups (MessagingGroupInfo as json string) + * @param friends list of friends (MessagingFriendInfo as json string) */ oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends); } \ No newline at end of file diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -20,6 +20,8 @@ "downloads": "Downloads", "features": "Features", "home": "Home", + "home_debug": "Debug", + "home_logs": "Logs", "social": "Social", "plugins": "Plugins" }, @@ -64,8 +66,8 @@ } }, - "action": { - "clean_cache": "Clean Cache", + "actions": { + "clean_snapchat_cache": "Clean Snapchat Cache", "clear_message_logger": "Clear Message Logger", "refresh_mappings": "Refresh Mappings", "open_map": "Choose location on map", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/Constants.kt b/core/src/main/kotlin/me/rhunk/snapenhance/Constants.kt @@ -1,14 +1,12 @@ package me.rhunk.snapenhance object Constants { - const val TAG = "SnapEnhance" const val SNAPCHAT_PACKAGE_NAME = "com.snapchat.android" const val VIEW_INJECTED_CODE = 0x7FFFFF02 val ARROYO_MEDIA_CONTAINER_PROTO_PATH = intArrayOf(4, 4) val ARROYO_STRING_CHAT_MESSAGE_PROTO = ARROYO_MEDIA_CONTAINER_PROTO_PATH + intArrayOf(2, 1) - val ARROYO_URL_KEY_PROTO_PATH = intArrayOf(4, 5, 1, 3) const val ENCRYPTION_PROTO_INDEX = 19 const val ENCRYPTION_PROTO_INDEX_V2 = 4 diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt @@ -2,47 +2,81 @@ package me.rhunk.snapenhance import android.util.Log import de.robv.android.xposed.XposedBridge -import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.bridge.BridgeClient -object Logger { - private const val TAG = "SnapEnhance" +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); - fun log(message: Any?) { - Log.i(TAG, message.toString()) - } + companion object { + fun fromLetter(letter: String): LogLevel? { + return values().find { it.letter == letter } + } - fun debug(message: Any?) { - if (!BuildConfig.DEBUG) return - Log.d(TAG, message.toString()) + fun fromShortName(shortName: String): LogLevel? { + return values().find { it.shortName == shortName } + } } +} - fun debug(tag: String, message: Any?) { - if (!BuildConfig.DEBUG) return - Log.d(tag, message.toString()) - } - fun error(throwable: Throwable) { - Log.e(TAG, "", throwable) - } +class Logger( + private val bridgeClient: BridgeClient +) { + companion object { + private const val TAG = "SnapEnhanceCore" - fun error(message: Any?) { - Log.e(TAG, message.toString()) - } + fun directDebug(message: Any?, tag: String = TAG) { + Log.println(Log.DEBUG, tag, message.toString()) + } - fun error(message: Any?, throwable: Throwable) { - Log.e(TAG, message.toString(), throwable) - } + 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?) { - XposedBridge.log(message.toString()) + fun xposedLog(message: Any?, throwable: Throwable, tag: String = TAG) { + Log.println(Log.INFO, tag, message.toString()) + XposedBridge.log("$tag: $message") + XposedBridge.log(throwable) + } } - fun xposedLog(message: Any?, throwable: Throwable?) { - XposedBridge.log(message.toString()) - XposedBridge.log(throwable) + private fun internalLog(tag: String, logLevel: LogLevel, message: Any?) { + runCatching { + bridgeClient.broadcastLog(tag, logLevel.shortName, message.toString()) + }.onFailure { + Log.println(logLevel.priority, tag, message.toString()) + } } - fun xposedLog(throwable: Throwable) { - XposedBridge.log(throwable) + 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) } + + 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,13 +11,13 @@ import android.widget.Toast import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.asCoroutineDispatcher -import me.rhunk.snapenhance.bridge.BridgeClient -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper -import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper +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.data.MessageSender -import me.rhunk.snapenhance.database.DatabaseAccess import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.manager.impl.ActionManager import me.rhunk.snapenhance.manager.impl.FeatureManager @@ -44,6 +44,7 @@ class ModContext { private val modConfig = ModConfig() val config by modConfig + val log by lazy { Logger(this.bridgeClient) } val event = EventBus(this) val eventDispatcher = EventDispatcher(this) val native = NativeLib() @@ -81,13 +82,13 @@ class ModContext { } } - fun shortToast(message: Any) { + fun shortToast(message: Any?) { runOnUiThread { Toast.makeText(androidContext, message.toString(), Toast.LENGTH_SHORT).show() } } - fun longToast(message: Any) { + fun longToast(message: Any?) { runOnUiThread { Toast.makeText(androidContext, message.toString(), Toast.LENGTH_LONG).show() } @@ -108,7 +109,7 @@ class ModContext { } fun crash(message: String, throwable: Throwable? = null) { - Logger.xposedLog(message, throwable) + Logger.xposedLog(message, throwable ?: Exception()) longToast(message) delayForceCloseApp(100) } @@ -123,6 +124,7 @@ class ModContext { } fun reloadConfig() { + log.verbose("reloading config") modConfig.loadFromBridge(bridgeClient) native.loadNativeConfig( NativeConfig( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -6,9 +6,9 @@ import android.content.Context import android.content.pm.PackageManager import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.bridge.BridgeClient import me.rhunk.snapenhance.bridge.SyncCallback import me.rhunk.snapenhance.core.BuildConfig +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 @@ -88,7 +88,7 @@ class SnapEnhance { return@hook } - Logger.debug("Reloading config") + appContext.actionManager.onNewIntent(activity.intent) appContext.reloadConfig() syncRemote() } @@ -114,7 +114,7 @@ class SnapEnhance { syncRemote() } }.also { time -> - Logger.debug("init took $time") + appContext.log.verbose("init took $time") } } @@ -126,7 +126,7 @@ class SnapEnhance { actionManager.init() } }.also { time -> - Logger.debug("onActivityCreate took $time") + appContext.log.verbose("onActivityCreate took $time") } } @@ -149,7 +149,6 @@ class SnapEnhance { val database = appContext.database appContext.executeAsync { - Logger.debug("request remote sync") appContext.bridgeClient.sync(object : SyncCallback.Stub() { override fun syncFriend(uuid: String): String? { return database.getFriendInfo(uuid)?.toJson() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt @@ -3,17 +3,10 @@ package me.rhunk.snapenhance.action import me.rhunk.snapenhance.ModContext import java.io.File -abstract class AbstractAction( - val nameKey: String -) { +abstract class AbstractAction{ lateinit var context: ModContext /** - * called on the main thread when the mod initialize - */ - open fun init() {} - - /** * called when the action is triggered */ open fun run() {} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/EnumAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/EnumAction.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.action + +import me.rhunk.snapenhance.action.impl.CleanCache +import me.rhunk.snapenhance.action.impl.ExportChatMessages +import me.rhunk.snapenhance.action.impl.OpenMap +import kotlin.reflect.KClass + +enum class EnumAction( + val key: String, + val clazz: KClass<out AbstractAction>, + val exitOnFinish: Boolean = false, + val isCritical: Boolean = false, +) { + CLEAN_CACHE("clean_snapchat_cache", CleanCache::class, exitOnFinish = true), + EXPORT_CHAT_MESSAGES("export_chat_messages", ExportChatMessages::class), + OPEN_MAP("open_map", OpenMap::class); +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt @@ -1,19 +0,0 @@ -package me.rhunk.snapenhance.action.impl - -import me.rhunk.snapenhance.action.AbstractAction -import me.rhunk.snapenhance.features.impl.AutoUpdater - -class CheckForUpdates : AbstractAction("action.check_for_updates") { - override fun run() { - context.executeAsync { - runCatching { - val latestVersion = context.feature(AutoUpdater::class).checkForUpdates() - if (latestVersion == null) { - context.longToast(context.translation["auto_updater.no_update_available"]) - } - }.onFailure { - context.longToast(it.message ?: "Failed to check for updates") - } - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.action.impl import me.rhunk.snapenhance.action.AbstractAction import java.io.File -class CleanCache : AbstractAction("action.clean_cache") { +class CleanCache : AbstractAction() { companion object { private val FILES = arrayOf( "files/mbgl-offline.db", @@ -22,7 +22,7 @@ class CleanCache : AbstractAction("action.clean_cache") { } override fun run() { - FILES.forEach {fileName -> + FILES.forEach { fileName -> val fileCache = File(context.androidContext.dataDir, fileName) if (fileName.endsWith("*")) { val parent = fileCache.parentFile ?: throw IllegalStateException("Parent file is null") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt @@ -1,10 +0,0 @@ -package me.rhunk.snapenhance.action.impl - -import me.rhunk.snapenhance.action.AbstractAction - -class ClearMessageLogger : AbstractAction("action.clear_message_logger") { - override fun run() { - context.bridgeClient.clearMessageLogger() - context.shortToast("Message logger cleared") - } -}- \ No newline at end of file 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 @@ -14,10 +14,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.action.AbstractAction +import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry 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.database.objects.FriendFeedEntry import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.util.CallbackBuilder @@ -26,7 +26,7 @@ import me.rhunk.snapenhance.util.export.MessageExporter import java.io.File @OptIn(DelicateCoroutinesApi::class) -class ExportChatMessages : AbstractAction("action.export_chat_messages") { +class ExportChatMessages : AbstractAction() { private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") } private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") } @@ -55,7 +55,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { context.runOnUiThread { if (dialogLogs.size > 15) dialogLogs.removeAt(0) dialogLogs.add(message) - Logger.debug("dialog: $message") + context.log.debug("dialog: $message") currentActionDialog!!.setMessage(dialogLogs.joinToString("\n")) } } @@ -198,7 +198,6 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { } while (true) { - Logger.debug("[$conversationName] fetching $lastMessageId") val messages = fetchMessagesPaginated(conversationId, lastMessageId) if (messages.isEmpty()) break foundMessages.addAll(messages) @@ -224,7 +223,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { it.readMessages(foundMessages) }.onFailure { logDialog(context.translation.format("chat_export.export_failed","conversation" to it.message.toString())) - Logger.error(it) + context.log.error("Failed to read messages", it) return } }.exportTo(exportType!!) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt @@ -5,7 +5,7 @@ import android.os.Bundle import me.rhunk.snapenhance.action.AbstractAction import me.rhunk.snapenhance.core.BuildConfig -class OpenMap: AbstractAction("action.open_map") { +class OpenMap: AbstractAction() { override fun run() { context.runOnUiThread { val mapActivityIntent = Intent() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt @@ -1,11 +0,0 @@ -package me.rhunk.snapenhance.action.impl - -import me.rhunk.snapenhance.action.AbstractAction -import me.rhunk.snapenhance.bridge.types.BridgeFileType - -class RefreshMappings : AbstractAction("action.refresh_mappings") { - override fun run() { - context.bridgeClient.deleteFile(BridgeFileType.MAPPINGS) - context.softRestartApp() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -1,145 +0,0 @@ -package me.rhunk.snapenhance.bridge - - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Build -import android.os.Handler -import android.os.HandlerThread -import android.os.IBinder -import de.robv.android.xposed.XposedHelpers -import me.rhunk.snapenhance.Logger.xposedLog -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.bridge.types.FileActionType -import me.rhunk.snapenhance.core.BuildConfig -import me.rhunk.snapenhance.core.messaging.MessagingRuleType -import me.rhunk.snapenhance.data.LocalePair -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors -import kotlin.system.exitProcess - - -class BridgeClient( - private val context: ModContext -): ServiceConnection { - private lateinit var future: CompletableFuture<Boolean> - private lateinit var service: BridgeInterface - - companion object { - const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.bridge.SYNC" - } - - fun start(callback: (Boolean) -> Unit) { - this.future = CompletableFuture() - - //TODO: randomize package name - with(context.androidContext) { - //ensure the remote process is running - startActivity(Intent() - .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.ForceStartActivity") - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) - ) - - val intent = Intent() - .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - bindService( - intent, - Context.BIND_AUTO_CREATE, - Executors.newSingleThreadExecutor(), - this@BridgeClient - ) - } else { - XposedHelpers.callMethod( - this, - "bindServiceAsUser", - intent, - this@BridgeClient, - Context.BIND_AUTO_CREATE, - Handler(HandlerThread("BridgeClient").apply { - start() - }.looper), - android.os.Process.myUserHandle() - ) - } - } - callback(future.get()) - } - - - override fun onServiceConnected(name: ComponentName, service: IBinder) { - this.service = BridgeInterface.Stub.asInterface(service) - future.complete(true) - } - - override fun onNullBinding(name: ComponentName) { - xposedLog("failed to connect to bridge service") - exitProcess(1) - } - - override fun onServiceDisconnected(name: ComponentName) { - exitProcess(0) - } - - fun createAndReadFile( - fileType: BridgeFileType, - defaultContent: ByteArray - ): ByteArray = service.fileOperation(FileActionType.CREATE_AND_READ.ordinal, fileType.value, defaultContent) - - fun readFile(fileType: BridgeFileType): ByteArray = service.fileOperation(FileActionType.READ.ordinal, fileType.value, null) - - fun writeFile( - fileType: BridgeFileType, - content: ByteArray? - ) { service.fileOperation(FileActionType.WRITE.ordinal, fileType.value, content) } - - fun deleteFile(fileType: BridgeFileType) { service.fileOperation(FileActionType.DELETE.ordinal, fileType.value, null) } - - fun isFileExists(fileType: BridgeFileType) = service.fileOperation(FileActionType.EXISTS.ordinal, fileType.value, null).isNotEmpty() - - fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit) - - fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? = service.getMessageLoggerMessage(conversationId, id) - - fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) = service.addMessageLoggerMessage(conversationId, id, message) - - fun deleteMessageLoggerMessage(conversationId: String, id: Long) = service.deleteMessageLoggerMessage(conversationId, id) - - fun clearMessageLogger() = service.clearMessageLogger() - - fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map { - LocalePair(it.key, it.value) - } - - fun getApplicationApkPath() = service.getApplicationApkPath() - - fun getAutoUpdaterTime(): Long { - createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { - return if (isEmpty()) { - 0 - } else { - String(this).toLong() - } - } - } - - fun setAutoUpdaterTime(time: Long) { - writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) - } - - fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) - - fun sync(callback: SyncCallback) = service.sync(callback) - - fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends) - - fun getRules(targetUuid: String): List<MessagingRuleType> { - return service.getRules(targetUuid).map { MessagingRuleType.getByName(it) } - } - - fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean) - = service.setRule(targetUuid, type.key, state) -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/FileLoaderWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/FileLoaderWrapper.kt @@ -1,35 +0,0 @@ -package me.rhunk.snapenhance.bridge - -import android.content.Context -import me.rhunk.snapenhance.bridge.types.BridgeFileType - -open class FileLoaderWrapper( - private val fileType: BridgeFileType, - private val defaultContent: ByteArray -) { - lateinit var isFileExists: () -> Boolean - lateinit var write: (ByteArray) -> Unit - lateinit var read: () -> ByteArray - lateinit var delete: () -> Unit - - fun loadFromContext(context: Context) { - val file = fileType.resolve(context) - isFileExists = { file.exists() } - read = { - if (!file.exists()) { - file.createNewFile() - file.writeBytes("{}".toByteArray(Charsets.UTF_8)) - } - file.readBytes() - } - write = { file.writeBytes(it) } - delete = { file.delete() } - } - - fun loadFromBridge(bridgeClient: BridgeClient) { - isFileExists = { bridgeClient.isFileExists(fileType) } - read = { bridgeClient.createAndReadFile(fileType, defaultContent) } - write = { bridgeClient.writeFile(fileType, it) } - delete = { bridgeClient.deleteFile(fileType) } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt @@ -1,25 +0,0 @@ -package me.rhunk.snapenhance.bridge.types - -import android.content.Context -import java.io.File - - -enum class BridgeFileType(val value: Int, val fileName: String, val displayName: String, private val isDatabase: Boolean = false) { - CONFIG(0, "config.json", "Config"), - MAPPINGS(1, "mappings.json", "Mappings"), - MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true), - AUTO_UPDATER_TIMESTAMP(3, "auto_updater_timestamp.txt", "Auto Updater Timestamp"), - PINNED_CONVERSATIONS(4, "pinned_conversations.txt", "Pinned Conversations"); - - fun resolve(context: Context): File = if (isDatabase) { - context.getDatabasePath(fileName) - } else { - File(context.filesDir, fileName) - } - - companion object { - fun fromValue(value: Int): BridgeFileType? { - return values().firstOrNull { it.value == value } - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/types/FileActionType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/types/FileActionType.kt @@ -1,5 +0,0 @@ -package me.rhunk.snapenhance.bridge.types - -enum class FileActionType { - CREATE_AND_READ, READ, WRITE, DELETE, EXISTS -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt @@ -1,100 +0,0 @@ -package me.rhunk.snapenhance.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.bridge.BridgeClient -import me.rhunk.snapenhance.data.LocalePair -import java.util.Locale - - -class LocaleWrapper { - companion object { - const val DEFAULT_LOCALE = "en_US" - - fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List<LocalePair> { - val locales = mutableListOf<LocalePair>().apply { - add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() })) - } - - if (locale == DEFAULT_LOCALE) return locales - - val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substring(0, 5) ?: return locales - - context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> - locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) - } - - return locales - } - - fun fetchAvailableLocales(context: Context): List<String> { - return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf() - } - } - - var userLocale = DEFAULT_LOCALE - - private val translationMap = linkedMapOf<String, String>() - - lateinit var loadedLocale: Locale - - private fun load(localePair: LocalePair) { - loadedLocale = localePair.locale.let { Locale(it.substring(0, 2), it.substring(3, 5)) } - - val translations = JsonParser.parseString(localePair.content).asJsonObject - if (translations == null || translations.isJsonNull) { - return - } - - fun scanObject(jsonObject: JsonObject, prefix: String = "") { - jsonObject.entrySet().forEach { - if (it.value.isJsonPrimitive) { - val key = "$prefix${it.key}" - translationMap[key] = it.value.asString - } - if (!it.value.isJsonObject) return@forEach - scanObject(it.value.asJsonObject, "$prefix${it.key}.") - } - } - - scanObject(translations) - } - - fun loadFromBridge(bridgeClient: BridgeClient) { - bridgeClient.fetchLocales(userLocale).forEach { - load(it) - } - } - - fun loadFromContext(context: Context) { - fetchLocales(context, userLocale).forEach { - load(it) - } - } - - fun reloadFromContext(context: Context, locale: String) { - userLocale = locale - translationMap.clear() - loadFromContext(context) - } - - operator fun get(key: String) = translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") } - - fun format(key: String, vararg args: Pair<String, String>): String { - return args.fold(get(key)) { acc, pair -> - acc.replace("{${pair.first}}", pair.second) - } - } - - fun getCategory(key: String): LocaleWrapper { - return LocaleWrapper().apply { - translationMap.putAll( - this@LocaleWrapper.translationMap - .filterKeys { it.startsWith("$key.") } - .mapKeys { it.key.substring(key.length + 1) } - ) - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt @@ -1,165 +0,0 @@ -package me.rhunk.snapenhance.bridge.wrapper - -import android.content.Context -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.bridge.FileLoaderWrapper -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapmapper.Mapper -import me.rhunk.snapmapper.impl.BCryptClassMapper -import me.rhunk.snapmapper.impl.CallbackMapper -import me.rhunk.snapmapper.impl.CompositeConfigurationProviderMapper -import me.rhunk.snapmapper.impl.DefaultMediaItemMapper -import me.rhunk.snapmapper.impl.EnumMapper -import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper -import me.rhunk.snapmapper.impl.MediaQualityLevelProviderMapper -import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper -import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper -import me.rhunk.snapmapper.impl.PlusSubscriptionMapper -import me.rhunk.snapmapper.impl.ScCameraSettingsMapper -import me.rhunk.snapmapper.impl.ScoreUpdateMapper -import me.rhunk.snapmapper.impl.StoryBoostStateMapper -import java.util.concurrent.ConcurrentHashMap -import kotlin.system.measureTimeMillis - -class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) { - companion object { - private val gson = GsonBuilder().setPrettyPrinting().create() - private val mappers = arrayOf( - BCryptClassMapper::class, - CallbackMapper::class, - DefaultMediaItemMapper::class, - MediaQualityLevelProviderMapper::class, - EnumMapper::class, - OperaPageViewControllerMapper::class, - PlatformAnalyticsCreatorMapper::class, - PlusSubscriptionMapper::class, - ScCameraSettingsMapper::class, - StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class, - CompositeConfigurationProviderMapper::class, - ScoreUpdateMapper::class - ) - } - - private lateinit var context: Context - - private val mappings = ConcurrentHashMap<String, Any>() - private var snapBuildNumber: Long = 0 - - fun init(context: Context) { - this.context = context - snapBuildNumber = getSnapchatVersionCode() - - if (isFileExists()) { - runCatching { - loadCached() - }.onFailure { - Logger.error("Failed to load cached mappings", it) - delete() - } - } else { - Logger.debug("Mappings file does not exist") - } - } - - fun getSnapchatPackageInfo() = runCatching { - context.packageManager.getPackageInfo( - Constants.SNAPCHAT_PACKAGE_NAME, - 0 - ) - }.getOrNull() - - fun getSnapchatVersionCode() = getSnapchatPackageInfo()?.longVersionCode ?: -1 - fun getApplicationSourceDir() = getSnapchatPackageInfo()?.applicationInfo?.sourceDir - fun getGeneratedBuildNumber() = snapBuildNumber - - fun isMappingsOutdated(): Boolean { - return snapBuildNumber != getSnapchatVersionCode() || isMappingsLoaded().not() - } - - fun isMappingsLoaded(): Boolean { - return mappings.isNotEmpty() - } - - private fun loadCached() { - if (!isFileExists()) { - throw Exception("Mappings file does not exist") - } - val mappingsObject = JsonParser.parseString(read().toString(Charsets.UTF_8)).asJsonObject.also { - snapBuildNumber = it["snap_build_number"].asLong - } - - mappingsObject.entrySet().forEach { (key, value): Map.Entry<String, JsonElement> -> - if (value.isJsonArray) { - mappings[key] = gson.fromJson(value, ArrayList::class.java) - return@forEach - } - if (value.isJsonObject) { - mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java) - return@forEach - } - mappings[key] = value.asString - } - } - - fun refresh() { - snapBuildNumber = getSnapchatVersionCode() - val mapper = Mapper(*mappers) - - runCatching { - mapper.loadApk(getApplicationSourceDir() ?: throw Exception("Failed to get APK")) - }.onFailure { - throw Exception("Failed to load APK", it) - } - - measureTimeMillis { - val result = mapper.start().apply { - addProperty("snap_build_number", snapBuildNumber) - } - write(result.toString().toByteArray()) - }.also { - Logger.debug("Generated mappings in $it ms") - } - } - - fun getMappedObject(key: String): Any { - if (mappings.containsKey(key)) { - return mappings[key]!! - } - throw Exception("No mapping found for $key") - } - - fun getMappedObjectNullable(key: String): Any? { - return mappings[key] - } - - fun getMappedClass(className: String): Class<*> { - return context.classLoader.loadClass(getMappedObject(className) as String) - } - - fun getMappedClass(key: String, subKey: String): Class<*> { - return context.classLoader.loadClass(getMappedValue(key, subKey)) - } - - fun getMappedValue(key: String): String { - return getMappedObject(key) as String - } - - @Suppress("UNCHECKED_CAST") - fun <T : Any> getMappedList(key: String): List<T> { - return listOf(getMappedObject(key) as List<T>).flatten() - } - - fun getMappedValue(key: String, subKey: String): String { - return getMappedMap(key)[subKey] as String - } - - @Suppress("UNCHECKED_CAST") - fun getMappedMap(key: String): Map<String, *> { - return getMappedObject(key) as Map<String, *> - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt @@ -1,70 +0,0 @@ -package me.rhunk.snapenhance.bridge.wrapper - -import android.content.ContentValues -import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.util.SQLiteDatabaseHelper -import java.io.File - -class MessageLoggerWrapper( - private val databaseFile: File -) { - - lateinit var database: SQLiteDatabase - - fun init() { - database = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE) - SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf( - "messages" to listOf( - "id INTEGER PRIMARY KEY", - "conversation_id VARCHAR", - "message_id BIGINT", - "message_data BLOB" - ) - )) - } - - fun deleteMessage(conversationId: String, messageId: Long) { - database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) - } - - fun addMessage(conversationId: String, messageId: Long, serializedMessage: ByteArray): Boolean { - val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) - val state = cursor.moveToFirst() - cursor.close() - if (state) { - return false - } - database.insert("messages", null, ContentValues().apply { - put("conversation_id", conversationId) - put("message_id", messageId) - put("message_data", serializedMessage) - }) - return true - } - - fun getMessage(conversationId: String, messageId: Long): Pair<Boolean, ByteArray?> { - val cursor = database.rawQuery("SELECT message_data FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) - val state = cursor.moveToFirst() - val message: ByteArray? = if (state) { - cursor.getBlob(0) - } else { - null - } - cursor.close() - return Pair(state, message) - } - - fun getMessageIds(conversationId: String, limit: Int): List<Long> { - val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? ORDER BY message_id DESC LIMIT ?", arrayOf(conversationId, limit.toString())) - val messageIds = mutableListOf<Long>() - while (cursor.moveToNext()) { - messageIds.add(cursor.getLong(0)) - } - cursor.close() - return messageIds - } - - fun clearMessages() { - database.execSQL("DELETE FROM messages") - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -0,0 +1,147 @@ +package me.rhunk.snapenhance.core.bridge + + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.BridgeInterface +import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.bridge.SyncCallback +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.bridge.types.FileActionType +import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.data.LocalePair +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import kotlin.system.exitProcess + + +class BridgeClient( + private val context: ModContext +): ServiceConnection { + private lateinit var future: CompletableFuture<Boolean> + private lateinit var service: BridgeInterface + + companion object { + const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.core.bridge.SYNC" + } + + fun start(callback: (Boolean) -> Unit) { + this.future = CompletableFuture() + + //TODO: randomize package name + with(context.androidContext) { + //ensure the remote process is running + startActivity(Intent() + .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.ForceStartActivity") + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + ) + + val intent = Intent() + .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + bindService( + intent, + Context.BIND_AUTO_CREATE, + Executors.newSingleThreadExecutor(), + this@BridgeClient + ) + } else { + XposedHelpers.callMethod( + this, + "bindServiceAsUser", + intent, + this@BridgeClient, + Context.BIND_AUTO_CREATE, + Handler(HandlerThread("BridgeClient").apply { + start() + }.looper), + android.os.Process.myUserHandle() + ) + } + } + callback(future.get()) + } + + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + this.service = BridgeInterface.Stub.asInterface(service) + future.complete(true) + } + + override fun onNullBinding(name: ComponentName) { + context.log.error("BridgeClient", "failed to connect to bridge service") + exitProcess(1) + } + + override fun onServiceDisconnected(name: ComponentName) { + exitProcess(0) + } + + fun broadcastLog(tag: String, level: String, message: String) = service.broadcastLog(tag, level, message) + + fun createAndReadFile( + fileType: BridgeFileType, + defaultContent: ByteArray + ): ByteArray = service.fileOperation(FileActionType.CREATE_AND_READ.ordinal, fileType.value, defaultContent) + + fun readFile(fileType: BridgeFileType): ByteArray = service.fileOperation(FileActionType.READ.ordinal, fileType.value, null) + + fun writeFile( + fileType: BridgeFileType, + content: ByteArray? + ) { service.fileOperation(FileActionType.WRITE.ordinal, fileType.value, content) } + + fun deleteFile(fileType: BridgeFileType) { service.fileOperation(FileActionType.DELETE.ordinal, fileType.value, null) } + + fun isFileExists(fileType: BridgeFileType) = service.fileOperation(FileActionType.EXISTS.ordinal, fileType.value, null).isNotEmpty() + + fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit) + + fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? = service.getMessageLoggerMessage(conversationId, id) + + fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) = service.addMessageLoggerMessage(conversationId, id, message) + + fun deleteMessageLoggerMessage(conversationId: String, id: Long) = service.deleteMessageLoggerMessage(conversationId, id) + + fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map { + LocalePair(it.key, it.value) + } + + fun getApplicationApkPath() = service.getApplicationApkPath() + + fun getAutoUpdaterTime(): Long { + createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { + return if (isEmpty()) { + 0 + } else { + String(this).toLong() + } + } + } + + fun setAutoUpdaterTime(time: Long) { + writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) + } + + fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) + + fun sync(callback: SyncCallback) = service.sync(callback) + + fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends) + + fun getRules(targetUuid: String): List<MessagingRuleType> { + return service.getRules(targetUuid).map { MessagingRuleType.getByName(it) } + } + + fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean) + = service.setRule(targetUuid, type.key, state) +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/FileLoaderWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/FileLoaderWrapper.kt @@ -0,0 +1,35 @@ +package me.rhunk.snapenhance.core.bridge + +import android.content.Context +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType + +open class FileLoaderWrapper( + private val fileType: BridgeFileType, + private val defaultContent: ByteArray +) { + lateinit var isFileExists: () -> Boolean + lateinit var write: (ByteArray) -> Unit + lateinit var read: () -> ByteArray + lateinit var delete: () -> Unit + + fun loadFromContext(context: Context) { + val file = fileType.resolve(context) + isFileExists = { file.exists() } + read = { + if (!file.exists()) { + file.createNewFile() + file.writeBytes("{}".toByteArray(Charsets.UTF_8)) + } + file.readBytes() + } + write = { file.writeBytes(it) } + delete = { file.delete() } + } + + fun loadFromBridge(bridgeClient: BridgeClient) { + isFileExists = { bridgeClient.isFileExists(fileType) } + read = { bridgeClient.createAndReadFile(fileType, defaultContent) } + write = { bridgeClient.writeFile(fileType, it) } + delete = { bridgeClient.deleteFile(fileType) } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt @@ -0,0 +1,25 @@ +package me.rhunk.snapenhance.core.bridge.types + +import android.content.Context +import java.io.File + + +enum class BridgeFileType(val value: Int, val fileName: String, val displayName: String, private val isDatabase: Boolean = false) { + CONFIG(0, "config.json", "Config"), + MAPPINGS(1, "mappings.json", "Mappings"), + MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true), + AUTO_UPDATER_TIMESTAMP(3, "auto_updater_timestamp.txt", "Auto Updater Timestamp"), + PINNED_CONVERSATIONS(4, "pinned_conversations.txt", "Pinned Conversations"); + + fun resolve(context: Context): File = if (isDatabase) { + context.getDatabasePath(fileName) + } else { + File(context.filesDir, fileName) + } + + companion object { + fun fromValue(value: Int): BridgeFileType? { + return values().firstOrNull { it.value == value } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/FileActionType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/FileActionType.kt @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.core.bridge.types + +enum class FileActionType { + CREATE_AND_READ, READ, WRITE, DELETE, EXISTS +}+ \ 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 @@ -0,0 +1,100 @@ +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.bridge.BridgeClient +import me.rhunk.snapenhance.data.LocalePair +import java.util.Locale + + +class LocaleWrapper { + companion object { + const val DEFAULT_LOCALE = "en_US" + + fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List<LocalePair> { + val locales = mutableListOf<LocalePair>().apply { + add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() })) + } + + if (locale == DEFAULT_LOCALE) return locales + + val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substring(0, 5) ?: return locales + + context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> + locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) + } + + return locales + } + + fun fetchAvailableLocales(context: Context): List<String> { + return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf() + } + } + + var userLocale = DEFAULT_LOCALE + + private val translationMap = linkedMapOf<String, String>() + + lateinit var loadedLocale: Locale + + private fun load(localePair: LocalePair) { + loadedLocale = localePair.locale.let { Locale(it.substring(0, 2), it.substring(3, 5)) } + + val translations = JsonParser.parseString(localePair.content).asJsonObject + if (translations == null || translations.isJsonNull) { + return + } + + fun scanObject(jsonObject: JsonObject, prefix: String = "") { + jsonObject.entrySet().forEach { + if (it.value.isJsonPrimitive) { + val key = "$prefix${it.key}" + translationMap[key] = it.value.asString + } + if (!it.value.isJsonObject) return@forEach + scanObject(it.value.asJsonObject, "$prefix${it.key}.") + } + } + + scanObject(translations) + } + + fun loadFromBridge(bridgeClient: BridgeClient) { + bridgeClient.fetchLocales(userLocale).forEach { + load(it) + } + } + + fun loadFromContext(context: Context) { + fetchLocales(context, userLocale).forEach { + load(it) + } + } + + fun reloadFromContext(context: Context, locale: String) { + userLocale = locale + translationMap.clear() + loadFromContext(context) + } + + operator fun get(key: String) = translationMap[key] ?: key.also { Logger.directDebug("Missing translation for $key") } + + fun format(key: String, vararg args: Pair<String, String>): String { + return args.fold(get(key)) { acc, pair -> + acc.replace("{${pair.first}}", pair.second) + } + } + + fun getCategory(key: String): LocaleWrapper { + return LocaleWrapper().apply { + translationMap.putAll( + this@LocaleWrapper.translationMap + .filterKeys { it.startsWith("$key.") } + .mapKeys { it.key.substring(key.length + 1) } + ) + } + } +}+ \ No newline at end of file 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 @@ -0,0 +1,162 @@ +package me.rhunk.snapenhance.core.bridge.wrapper + +import android.content.Context +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.bridge.FileLoaderWrapper +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType +import me.rhunk.snapmapper.Mapper +import me.rhunk.snapmapper.impl.BCryptClassMapper +import me.rhunk.snapmapper.impl.CallbackMapper +import me.rhunk.snapmapper.impl.CompositeConfigurationProviderMapper +import me.rhunk.snapmapper.impl.DefaultMediaItemMapper +import me.rhunk.snapmapper.impl.EnumMapper +import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper +import me.rhunk.snapmapper.impl.MediaQualityLevelProviderMapper +import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper +import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper +import me.rhunk.snapmapper.impl.PlusSubscriptionMapper +import me.rhunk.snapmapper.impl.ScCameraSettingsMapper +import me.rhunk.snapmapper.impl.ScoreUpdateMapper +import me.rhunk.snapmapper.impl.StoryBoostStateMapper +import java.util.concurrent.ConcurrentHashMap +import kotlin.system.measureTimeMillis + +class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) { + companion object { + private val gson = GsonBuilder().setPrettyPrinting().create() + private val mappers = arrayOf( + BCryptClassMapper::class, + CallbackMapper::class, + DefaultMediaItemMapper::class, + MediaQualityLevelProviderMapper::class, + EnumMapper::class, + OperaPageViewControllerMapper::class, + PlatformAnalyticsCreatorMapper::class, + PlusSubscriptionMapper::class, + ScCameraSettingsMapper::class, + StoryBoostStateMapper::class, + FriendsFeedEventDispatcherMapper::class, + CompositeConfigurationProviderMapper::class, + ScoreUpdateMapper::class + ) + } + + private lateinit var context: Context + + private val mappings = ConcurrentHashMap<String, Any>() + private var snapBuildNumber: Long = 0 + + fun init(context: Context) { + this.context = context + snapBuildNumber = getSnapchatVersionCode() + + if (isFileExists()) { + runCatching { + loadCached() + }.onFailure { + delete() + } + } + } + + fun getSnapchatPackageInfo() = runCatching { + context.packageManager.getPackageInfo( + Constants.SNAPCHAT_PACKAGE_NAME, + 0 + ) + }.getOrNull() + + fun getSnapchatVersionCode() = getSnapchatPackageInfo()?.longVersionCode ?: -1 + fun getApplicationSourceDir() = getSnapchatPackageInfo()?.applicationInfo?.sourceDir + fun getGeneratedBuildNumber() = snapBuildNumber + + fun isMappingsOutdated(): Boolean { + return snapBuildNumber != getSnapchatVersionCode() || isMappingsLoaded().not() + } + + fun isMappingsLoaded(): Boolean { + return mappings.isNotEmpty() + } + + private fun loadCached() { + if (!isFileExists()) { + throw Exception("Mappings file does not exist") + } + val mappingsObject = JsonParser.parseString(read().toString(Charsets.UTF_8)).asJsonObject.also { + snapBuildNumber = it["snap_build_number"].asLong + } + + mappingsObject.entrySet().forEach { (key, value): Map.Entry<String, JsonElement> -> + if (value.isJsonArray) { + mappings[key] = gson.fromJson(value, ArrayList::class.java) + return@forEach + } + if (value.isJsonObject) { + mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java) + return@forEach + } + mappings[key] = value.asString + } + } + + fun refresh() { + snapBuildNumber = getSnapchatVersionCode() + val mapper = Mapper(*mappers) + + runCatching { + mapper.loadApk(getApplicationSourceDir() ?: throw Exception("Failed to get APK")) + }.onFailure { + throw Exception("Failed to load APK", it) + } + + measureTimeMillis { + val result = mapper.start().apply { + addProperty("snap_build_number", snapBuildNumber) + } + write(result.toString().toByteArray()) + }.also { + Logger.directDebug("Generated mappings in $it ms") + } + } + + fun getMappedObject(key: String): Any { + if (mappings.containsKey(key)) { + return mappings[key]!! + } + throw Exception("No mapping found for $key") + } + + fun getMappedObjectNullable(key: String): Any? { + return mappings[key] + } + + fun getMappedClass(className: String): Class<*> { + return context.classLoader.loadClass(getMappedObject(className) as String) + } + + fun getMappedClass(key: String, subKey: String): Class<*> { + return context.classLoader.loadClass(getMappedValue(key, subKey)) + } + + fun getMappedValue(key: String): String { + return getMappedObject(key) as String + } + + @Suppress("UNCHECKED_CAST") + fun <T : Any> getMappedList(key: String): List<T> { + return listOf(getMappedObject(key) as List<T>).flatten() + } + + fun getMappedValue(key: String, subKey: String): String { + return getMappedMap(key)[subKey] as String + } + + @Suppress("UNCHECKED_CAST") + fun getMappedMap(key: String): Map<String, *> { + return getMappedObject(key) as Map<String, *> + } +}+ \ No newline at end of file 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 @@ -0,0 +1,70 @@ +package me.rhunk.snapenhance.core.bridge.wrapper + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import me.rhunk.snapenhance.util.SQLiteDatabaseHelper +import java.io.File + +class MessageLoggerWrapper( + private val databaseFile: File +) { + + lateinit var database: SQLiteDatabase + + fun init() { + database = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE) + SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf( + "messages" to listOf( + "id INTEGER PRIMARY KEY", + "conversation_id VARCHAR", + "message_id BIGINT", + "message_data BLOB" + ) + )) + } + + fun deleteMessage(conversationId: String, messageId: Long) { + database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) + } + + fun addMessage(conversationId: String, messageId: Long, serializedMessage: ByteArray): Boolean { + val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) + val state = cursor.moveToFirst() + cursor.close() + if (state) { + return false + } + database.insert("messages", null, ContentValues().apply { + put("conversation_id", conversationId) + put("message_id", messageId) + put("message_data", serializedMessage) + }) + return true + } + + fun getMessage(conversationId: String, messageId: Long): Pair<Boolean, ByteArray?> { + val cursor = database.rawQuery("SELECT message_data FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) + val state = cursor.moveToFirst() + val message: ByteArray? = if (state) { + cursor.getBlob(0) + } else { + null + } + cursor.close() + return Pair(state, message) + } + + fun getMessageIds(conversationId: String, limit: Int): List<Long> { + val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? ORDER BY message_id DESC LIMIT ?", arrayOf(conversationId, limit.toString())) + val messageIds = mutableListOf<Long>() + while (cursor.moveToNext()) { + messageIds.add(cursor.getLong(0)) + } + cursor.close() + return messageIds + } + + fun clearMessages() { + database.execSQL("DELETE FROM messages") + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.core.config -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import kotlin.reflect.KProperty diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt @@ -4,11 +4,10 @@ import android.content.Context import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonObject -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.bridge.BridgeClient -import me.rhunk.snapenhance.bridge.FileLoaderWrapper -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.bridge.BridgeClient +import me.rhunk.snapenhance.core.bridge.FileLoaderWrapper +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.config.impl.RootConfig import kotlin.properties.Delegates @@ -33,7 +32,6 @@ class ModConfig { runCatching { loadConfig() }.onFailure { - Logger.error("Failed to load config", it) writeConfig() } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.core.config.impl import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.core.config.FeatureNotice class Experimental : ConfigContainer() { val nativeHooks = container("native_hooks", NativeHooks()) { icon = "Memory" } @@ -9,6 +10,6 @@ class Experimental : ConfigContainer() { val appLockOnResume = boolean("app_lock_on_resume") val infiniteStoryBoost = boolean("infinite_story_boost") val meoPasscodeBypass = boolean("meo_passcode_bypass") - val unlimitedMultiSnap = boolean("unlimited_multi_snap") + val unlimitedMultiSnap = boolean("unlimited_multi_snap") { addNotices(FeatureNotice.MAY_BAN)} val noFriendScoreDelay = boolean("no_friend_score_delay") } \ No newline at end of file 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 @@ -0,0 +1,257 @@ +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.database.objects.ConversationMessage +import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry +import me.rhunk.snapenhance.core.database.objects.FriendInfo +import me.rhunk.snapenhance.core.database.objects.StoryEntry +import me.rhunk.snapenhance.core.database.objects.UserConversationLink +import me.rhunk.snapenhance.manager.Manager +import java.io.File + +@SuppressLint("Range") +class DatabaseAccess(private val context: ModContext) : Manager { + private val databaseLock = Any() + + private val arroyoDatabase: File by lazy { + context.androidContext.getDatabasePath("arroyo.db") + } + + private val mainDatabase: File by lazy { + context.androidContext.getDatabasePath("main.db") + } + + private fun openMain(): SQLiteDatabase { + return SQLiteDatabase.openDatabase( + mainDatabase.absolutePath, + null, + SQLiteDatabase.OPEN_READONLY + )!! + } + + private fun openArroyo(): SQLiteDatabase { + return SQLiteDatabase.openDatabase( + arroyoDatabase.absolutePath, + null, + SQLiteDatabase.OPEN_READONLY + )!! + } + + fun hasArroyo(): Boolean { + return arroyoDatabase.exists() + } + + private fun <T> safeDatabaseOperation( + database: SQLiteDatabase, + query: (SQLiteDatabase) -> T? + ): T? { + synchronized(databaseLock) { + return runCatching { + query(database) + }.onFailure { + Logger.xposedLog("Database operation failed", it) + }.getOrNull() + } + } + + private fun <T : DatabaseObject> readDatabaseObject( + obj: T, + database: SQLiteDatabase, + table: String, + where: String, + args: Array<String> + ): T? { + val cursor = database.rawQuery("SELECT * FROM $table WHERE $where", args) + if (!cursor.moveToFirst()) { + cursor.close() + return null + } + try { + obj.write(cursor) + } catch (e: Throwable) { + context.log.error("Failed to read database object", e) + } + cursor.close() + return obj + } + + fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { + return safeDatabaseOperation(openMain()) { database -> + readDatabaseObject( + FriendFeedEntry(), + database, + "FriendsFeedView", + "friendUserId = ?", + arrayOf(userId) + ) + } + } + + val myUserId by lazy { + safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery(buildString { + append("SELECT * FROM required_values WHERE key = 'USERID'") + }, null) + + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation null + } + + val userId = cursor.getString(cursor.getColumnIndex("value")) + cursor.close() + userId + }!! + } + + fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { + return safeDatabaseOperation(openMain()) { + readDatabaseObject( + FriendFeedEntry(), + it, + "FriendsFeedView", + "key = ?", + arrayOf(conversationId) + ) + } + } + + fun getFriendInfo(userId: String): FriendInfo? { + return safeDatabaseOperation(openMain()) { + readDatabaseObject( + FriendInfo(), + it, + "FriendWithUsername", + "userId = ?", + arrayOf(userId) + ) + } + } + + fun getFeedEntries(limit: Int): List<FriendFeedEntry> { + return safeDatabaseOperation(openMain()) { database -> + val cursor = database.rawQuery( + "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", + arrayOf(limit.toString()) + ) + val list = mutableListOf<FriendFeedEntry>() + while (cursor.moveToNext()) { + val friendFeedEntry = FriendFeedEntry() + try { + friendFeedEntry.write(cursor) + } catch (_: Throwable) {} + list.add(friendFeedEntry) + } + cursor.close() + list + } ?: emptyList() + } + + fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { + return safeDatabaseOperation(openArroyo()) { + readDatabaseObject( + ConversationMessage(), + it, + "conversation_message", + "client_message_id = ?", + arrayOf(clientMessageId.toString()) + ) + } + } + + fun getConversationType(conversationId: String): Int? { + return safeDatabaseOperation(openArroyo()) { + val cursor = it.rawQuery( + "SELECT * FROM user_conversation WHERE client_conversation_id = ?", + arrayOf(conversationId) + ) + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation null + } + val type = cursor.getInt(cursor.getColumnIndex("conversation_type")) + cursor.close() + type + } + } + + fun getConversationLinkFromUserId(userId: String): UserConversationLink? { + return safeDatabaseOperation(openArroyo()) { + readDatabaseObject( + UserConversationLink(), + it, + "user_conversation", + "user_id = ? AND conversation_type = 0", + arrayOf(userId) + ) + } + } + + fun getDMOtherParticipant(conversationId: String): String? { + return safeDatabaseOperation(openArroyo()) { cursor -> + val query = cursor.rawQuery( + "SELECT * FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", + arrayOf(conversationId) + ) + val participants = mutableListOf<String>() + while (query.moveToNext()) { + participants.add(query.getString(query.getColumnIndex("user_id"))) + } + query.close() + participants.firstOrNull { it != myUserId } + } + } + + + fun getStoryEntryFromId(storyId: String): StoryEntry? { + return safeDatabaseOperation(openMain()) { + readDatabaseObject(StoryEntry(), it, "Story", "storyId = ?", arrayOf(storyId)) + } + } + + fun getConversationParticipants(conversationId: String): List<String>? { + return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery( + "SELECT * FROM user_conversation WHERE client_conversation_id = ?", + arrayOf(conversationId) + ) + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation emptyList() + } + val participants = mutableListOf<String>() + do { + participants.add(cursor.getString(cursor.getColumnIndex("user_id"))) + } while (cursor.moveToNext()) + cursor.close() + participants + } + } + + fun getMessagesFromConversationId( + conversationId: String, + limit: Int + ): List<ConversationMessage>? { + return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery( + "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", + arrayOf(conversationId, limit.toString()) + ) + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation emptyList() + } + val messages = mutableListOf<ConversationMessage>() + do { + val message = ConversationMessage() + message.write(cursor) + messages.add(message) + } while (cursor.moveToNext()) + cursor.close() + messages + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseObject.kt @@ -0,0 +1,7 @@ +package me.rhunk.snapenhance.core.database + +import android.database.Cursor + +interface DatabaseObject { + fun write(cursor: Cursor) +} 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 @@ -0,0 +1,50 @@ +package me.rhunk.snapenhance.core.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.database.DatabaseObject +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( + var clientConversationId: String? = null, + var clientMessageId: Int = 0, + var serverMessageId: Int = 0, + var messageContent: ByteArray? = null, + var isSaved: Int = 0, + var isViewedByUser: Int = 0, + var contentType: Int = 0, + var creationTimestamp: Long = 0, + var readTimestamp: Long = 0, + var senderId: String? = null +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + with(cursor) { + clientConversationId = getStringOrNull("client_conversation_id") + clientMessageId = getInteger("client_message_id") + serverMessageId = getInteger("server_message_id") + messageContent = getBlobOrNull("message_content") + isSaved = getInteger("is_saved") + isViewedByUser = getInteger("is_viewed_by_user") + contentType = getInteger("content_type") + creationTimestamp = getLong("creation_timestamp") + readTimestamp = getLong("read_timestamp") + senderId = getStringOrNull("sender_id") + } + } + + fun getMessageAsString(): String? { + return when (ContentType.fromId(contentType)) { + ContentType.CHAT -> messageContent?.let { ProtoReader(it).getString(*Constants.ARROYO_STRING_CHAT_MESSAGE_PROTO) } + else -> null + } + } +} 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 @@ -0,0 +1,47 @@ +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 + +data class FriendFeedEntry( + var id: Int = 0, + var feedDisplayName: String? = null, + var participantsSize: Int = 0, + var lastInteractionTimestamp: Long = 0, + var displayTimestamp: Long = 0, + var displayInteractionType: String? = null, + var lastInteractionUserId: Int? = null, + var key: String? = null, + var friendUserId: String? = null, + var friendDisplayName: String? = null, + var friendDisplayUsername: String? = null, + var friendLinkType: Int? = null, + var bitmojiAvatarId: String? = null, + var bitmojiSelfieId: String? = null, +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + with(cursor) { + id = getInteger("_id") + feedDisplayName = getStringOrNull("feedDisplayName") + participantsSize = getInteger("participantsSize") + lastInteractionTimestamp = getLong("lastInteractionTimestamp") + displayTimestamp = getLong("displayTimestamp") + displayInteractionType = getStringOrNull("displayInteractionType") + lastInteractionUserId = getIntOrNull("lastInteractionUserId") + key = getStringOrNull("key") + friendUserId = getStringOrNull("friendUserId") + friendDisplayName = getStringOrNull("friendDisplayName") + friendDisplayUsername = getStringOrNull("friendDisplayUsername") + friendLinkType = getIntOrNull("friendLinkType") + bitmojiAvatarId = getStringOrNull("bitmojiAvatarId") + bitmojiSelfieId = getStringOrNull("bitmojiSelfieId") + } + } +} 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 @@ -0,0 +1,64 @@ +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 + +data class FriendInfo( + var id: Int = 0, + var lastModifiedTimestamp: Long = 0, + var username: String? = null, + var userId: String? = null, + var displayName: String? = null, + var bitmojiAvatarId: String? = null, + var bitmojiSelfieId: String? = null, + var bitmojiSceneId: String? = null, + var bitmojiBackgroundId: String? = null, + var friendmojis: String? = null, + var friendmojiCategories: String? = null, + var snapScore: Int = 0, + var birthday: Long = 0, + var addedTimestamp: Long = 0, + var reverseAddedTimestamp: Long = 0, + var serverDisplayName: String? = null, + var streakLength: Int = 0, + var streakExpirationTimestamp: Long = 0, + var reverseBestFriendRanking: Int = 0, + var isPinnedBestFriend: Int = 0, + var plusBadgeVisibility: Int = 0, + var usernameForSorting: String? = null +) : DatabaseObject, SerializableDataObject() { + @SuppressLint("Range") + override fun write(cursor: Cursor) { + with(cursor) { + id = getInteger("_id") + lastModifiedTimestamp = getLong("_lastModifiedTimestamp") + username = getStringOrNull("username") + userId = getStringOrNull("userId") + displayName = getStringOrNull("displayName") + bitmojiAvatarId = getStringOrNull("bitmojiAvatarId") + bitmojiSelfieId = getStringOrNull("bitmojiSelfieId") + bitmojiSceneId = getStringOrNull("bitmojiSceneId") + bitmojiBackgroundId = getStringOrNull("bitmojiBackgroundId") + friendmojis = getStringOrNull("friendmojis") + friendmojiCategories = getStringOrNull("friendmojiCategories") + snapScore = getInteger("score") + birthday = getLong("birthday") + addedTimestamp = getLong("addedTimestamp") + reverseAddedTimestamp = getLong("reverseAddedTimestamp") + serverDisplayName = getStringOrNull("serverDisplayName") + streakLength = getInteger("streakLength") + streakExpirationTimestamp = getLong("streakExpiration") + reverseBestFriendRanking = getInteger("reverseBestFriendRanking") + usernameForSorting = getStringOrNull("usernameForSorting") + if (getColumnIndex("isPinnedBestFriend") != -1) isPinnedBestFriend = + getInteger("isPinnedBestFriend") + if (getColumnIndex("plusBadgeVisibility") != -1) plusBadgeVisibility = + getInteger("plusBadgeVisibility") + } + } +} 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 @@ -0,0 +1,27 @@ +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 + +data class StoryEntry( + var id: Int = 0, + var storyId: String? = null, + var displayName: String? = null, + var isLocal: Boolean? = null, + var userId: String? = null +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + with(cursor) { + id = getInteger("_id") + storyId = getStringOrNull("storyId") + displayName = getStringOrNull("displayName") + isLocal = getInteger("isLocal") == 1 + userId = getStringOrNull("userId") + } + } +} 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 @@ -0,0 +1,23 @@ +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 + +class UserConversationLink( + var userId: String? = null, + var clientConversationId: String? = null, + var conversationType: Int = 0 +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + with(cursor) { + userId = getStringOrNull("user_id") + clientConversationId = getStringOrNull("client_conversation_id") + conversationType = getInteger("conversation_type") + } + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadManagerClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadManagerClient.kt @@ -0,0 +1,73 @@ +package me.rhunk.snapenhance.core.download + +import android.content.Intent +import android.os.Bundle +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.core.download.data.DashOptions +import me.rhunk.snapenhance.core.download.data.DownloadMediaType +import me.rhunk.snapenhance.core.download.data.DownloadMetadata +import me.rhunk.snapenhance.core.download.data.DownloadRequest +import me.rhunk.snapenhance.core.download.data.InputMedia +import me.rhunk.snapenhance.core.download.data.MediaEncryptionKeyPair + +class DownloadManagerClient ( + private val context: ModContext, + private val metadata: DownloadMetadata, + private val callback: DownloadCallback +) { + companion object { + const val DOWNLOAD_REQUEST_EXTRA = "request" + const val DOWNLOAD_METADATA_EXTRA = "metadata" + } + + private fun enqueueDownloadRequest(request: DownloadRequest) { + context.bridgeClient.enqueueDownload(Intent().apply { + putExtras(Bundle().apply { + putString(DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request)) + putString(DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata)) + }) + }, callback) + } + + fun downloadDashMedia(playlistUrl: String, offsetTime: Long, duration: Long?) { + enqueueDownloadRequest( + DownloadRequest( + inputMedias = arrayOf( + InputMedia( + content = playlistUrl, + type = DownloadMediaType.REMOTE_MEDIA + ) + ), + dashOptions = DashOptions(offsetTime, duration), + flags = DownloadRequest.Flags.IS_DASH_PLAYLIST + ) + ) + } + + fun downloadSingleMedia(mediaData: String, mediaType: DownloadMediaType, encryption: MediaEncryptionKeyPair? = null) { + enqueueDownloadRequest( + DownloadRequest( + inputMedias = arrayOf( + InputMedia( + content = mediaData, + type = mediaType, + encryption = encryption + ) + ) + ) + ) + } + + fun downloadMediaWithOverlay( + original: InputMedia, + overlay: InputMedia, + ) { + enqueueDownloadRequest( + DownloadRequest( + inputMedias = arrayOf(original, overlay), + flags = DownloadRequest.Flags.MERGE_OVERLAY + ) + ) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadTaskManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadTaskManager.kt @@ -0,0 +1,182 @@ +package me.rhunk.snapenhance.core.download + +import android.annotation.SuppressLint +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import me.rhunk.snapenhance.core.download.data.DownloadMetadata +import me.rhunk.snapenhance.core.download.data.DownloadObject +import me.rhunk.snapenhance.core.download.data.DownloadStage +import me.rhunk.snapenhance.core.download.data.MediaFilter +import me.rhunk.snapenhance.util.SQLiteDatabaseHelper +import me.rhunk.snapenhance.util.ktx.getIntOrNull +import me.rhunk.snapenhance.util.ktx.getStringOrNull + +class DownloadTaskManager { + private lateinit var taskDatabase: SQLiteDatabase + private val pendingTasks = mutableMapOf<Int, DownloadObject>() + private val cachedTasks = mutableMapOf<Int, DownloadObject>() + + @SuppressLint("Range") + fun init(context: Context) { + if (this::taskDatabase.isInitialized) return + taskDatabase = context.openOrCreateDatabase("download_tasks", Context.MODE_PRIVATE, null).apply { + SQLiteDatabaseHelper.createTablesFromSchema(this, mapOf( + "tasks" to listOf( + "id INTEGER PRIMARY KEY AUTOINCREMENT", + "hash VARCHAR UNIQUE", + "outputPath TEXT", + "outputFile TEXT", + "mediaDisplayType TEXT", + "mediaDisplaySource TEXT", + "iconUrl TEXT", + "downloadStage TEXT" + ) + )) + } + } + + fun addTask(task: DownloadObject): Int { + taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)", + arrayOf( + task.metadata.mediaIdentifier, + task.metadata.outputPath, + task.outputFile, + task.metadata.mediaDisplayType, + task.metadata.mediaDisplaySource, + task.metadata.iconUrl, + task.downloadStage.name + ) + ) + task.downloadId = taskDatabase.rawQuery("SELECT last_insert_rowid()", null).use { + it.moveToFirst() + it.getInt(0) + } + pendingTasks[task.downloadId] = task + return task.downloadId + } + + fun updateTask(task: DownloadObject) { + taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?", + arrayOf( + task.metadata.mediaIdentifier, + task.metadata.outputPath, + task.outputFile, + task.metadata.mediaDisplayType, + task.metadata.mediaDisplaySource, + task.metadata.iconUrl, + task.downloadStage.name, + task.downloadId + ) + ) + //if the task is no longer active, move it to the cached tasks + if (task.isJobActive()) { + pendingTasks[task.downloadId] = task + } else { + pendingTasks.remove(task.downloadId) + cachedTasks[task.downloadId] = task + } + } + + @SuppressLint("Range") + fun canDownloadMedia(mediaIdentifier: String?): DownloadStage? { + if (mediaIdentifier == null) return null + + val cursor = taskDatabase.rawQuery("SELECT * FROM tasks WHERE hash = ?", arrayOf(mediaIdentifier)) + if (cursor.count > 0) { + cursor.moveToFirst() + val downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage"))) + cursor.close() + + //if the stage has reached a final stage and is not in a saved state, remove the task + if (downloadStage.isFinalStage && downloadStage != DownloadStage.SAVED) { + taskDatabase.execSQL("DELETE FROM tasks WHERE hash = ?", arrayOf(mediaIdentifier)) + return null + } + + return downloadStage + } + cursor.close() + return null + } + + fun isEmpty(): Boolean { + return cachedTasks.isEmpty() && pendingTasks.isEmpty() + } + + private fun removeTask(id: Int) { + taskDatabase.execSQL("DELETE FROM tasks WHERE id = ?", arrayOf(id)) + cachedTasks.remove(id) + pendingTasks.remove(id) + } + + fun removeTask(task: DownloadObject) { + removeTask(task.downloadId) + } + + fun queryFirstTasks(filter: MediaFilter): Map<Int, DownloadObject> { + val isPendingFilter = filter == MediaFilter.PENDING + val tasks = mutableMapOf<Int, DownloadObject>() + + tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.mediaDisplayType) }) + if (isPendingFilter) { + return tasks.toSortedMap(reverseOrder()) + } + + tasks.putAll(queryTasks( + from = tasks.values.lastOrNull()?.downloadId ?: Int.MAX_VALUE, + amount = 30, + filter = filter + )) + + return tasks.toSortedMap(reverseOrder()) + } + + @SuppressLint("Range") + fun queryTasks(from: Int, amount: Int = 30, filter: MediaFilter = MediaFilter.NONE): Map<Int, DownloadObject> { + if (filter == MediaFilter.PENDING) { + return emptyMap() + } + + val cursor = taskDatabase.rawQuery( + "SELECT * FROM tasks WHERE id < ? AND mediaDisplayType LIKE ? ORDER BY id DESC LIMIT ?", + arrayOf( + from.toString(), + if (filter.shouldIgnoreFilter) "%" else "%${filter.key}", + amount.toString() + ) + ) + + val result = sortedMapOf<Int, DownloadObject>() + + while (cursor.moveToNext()) { + val task = DownloadObject( + downloadId = cursor.getIntOrNull("id")!!, + outputFile = cursor.getStringOrNull("outputFile"), + metadata = DownloadMetadata( + outputPath = cursor.getStringOrNull("outputPath")!!, + mediaIdentifier = cursor.getStringOrNull("hash"), + mediaDisplayType = cursor.getStringOrNull("mediaDisplayType"), + mediaDisplaySource = cursor.getStringOrNull("mediaDisplaySource"), + iconUrl = cursor.getStringOrNull("iconUrl") + ) + ).apply { + downloadTaskManager = this@DownloadTaskManager + downloadStage = DownloadStage.valueOf(cursor.getStringOrNull("downloadStage")!!) + //if downloadStage is not saved, it means the app was killed before the download was finished + if (downloadStage != DownloadStage.SAVED) { + downloadStage = DownloadStage.FAILED + } + } + result[task.downloadId] = task + } + cursor.close() + + return result.toSortedMap(reverseOrder()) + } + + fun removeAllTasks() { + taskDatabase.execSQL("DELETE FROM tasks") + cachedTasks.clear() + pendingTasks.clear() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMediaType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMediaType.kt @@ -0,0 +1,22 @@ +package me.rhunk.snapenhance.core.download.data + +import android.net.Uri + +enum class DownloadMediaType { + PROTO_MEDIA, + DIRECT_MEDIA, + REMOTE_MEDIA, + LOCAL_MEDIA; + + companion object { + fun fromUri(uri: Uri): DownloadMediaType { + return when (uri.scheme) { + "proto" -> PROTO_MEDIA + "direct" -> DIRECT_MEDIA + "http", "https" -> REMOTE_MEDIA + "file" -> LOCAL_MEDIA + else -> throw IllegalArgumentException("Unknown uri scheme: ${uri.scheme}") + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.core.download.data + +data class DownloadMetadata( + val mediaIdentifier: String?, + val outputPath: String, + val mediaDisplaySource: String?, + val mediaDisplayType: String?, + val iconUrl: String? +)+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadObject.kt @@ -0,0 +1,32 @@ +package me.rhunk.snapenhance.core.download.data + +import kotlinx.coroutines.Job +import me.rhunk.snapenhance.core.download.DownloadTaskManager + +data class DownloadObject( + var downloadId: Int = 0, + var outputFile: String? = null, + val metadata : DownloadMetadata +) { + lateinit var downloadTaskManager: DownloadTaskManager + var job: Job? = null + + var changeListener = { _: DownloadStage, _: DownloadStage -> } + private var _stage: DownloadStage = DownloadStage.PENDING + var downloadStage: DownloadStage + get() = synchronized(this) { + _stage + } + set(value) = synchronized(this) { + changeListener(_stage, value) + _stage = value + downloadTaskManager.updateTask(this) + } + + fun isJobActive() = job?.isActive == true + + fun cancel() { + downloadStage = DownloadStage.CANCELLED + job?.cancel() + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadRequest.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadRequest.kt @@ -0,0 +1,26 @@ +package me.rhunk.snapenhance.core.download.data + + +data class DashOptions(val offsetTime: Long, val duration: Long?) +data class InputMedia( + val content: String, + val type: DownloadMediaType, + val encryption: MediaEncryptionKeyPair? = null +) + +class DownloadRequest( + val inputMedias: Array<InputMedia>, + val dashOptions: DashOptions? = null, + private val flags: Int = 0, +) { + object Flags { + const val MERGE_OVERLAY = 1 + const val IS_DASH_PLAYLIST = 2 + } + + val isDashPlaylist: Boolean + get() = flags and Flags.IS_DASH_PLAYLIST != 0 + + val shouldMergeOverlay: Boolean + get() = flags and Flags.MERGE_OVERLAY != 0 +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadStage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadStage.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.core.download.data + +enum class DownloadStage( + val isFinalStage: Boolean = false, +) { + PENDING(false), + DOWNLOADING(false), + MERGING(false), + DOWNLOADED(true), + SAVED(true), + MERGE_FAILED(true), + FAILED(true), + CANCELLED(true) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaEncryptionKeyPair.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaEncryptionKeyPair.kt @@ -0,0 +1,21 @@ +@file:OptIn(ExperimentalEncodingApi::class) + +package me.rhunk.snapenhance.core.download.data + +import me.rhunk.snapenhance.data.wrapper.impl.media.EncryptionWrapper +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +// key and iv are base64 encoded +data class MediaEncryptionKeyPair( + val key: String, + val iv: String +) + +fun Pair<ByteArray, ByteArray>.toKeyPair(): MediaEncryptionKeyPair { + return MediaEncryptionKeyPair(Base64.UrlSafe.encode(this.first), Base64.UrlSafe.encode(this.second)) +} + +fun EncryptionWrapper.toKeyPair(): MediaEncryptionKeyPair { + return MediaEncryptionKeyPair(Base64.UrlSafe.encode(this.keySpec), Base64.UrlSafe.encode(this.ivKeyParameterSpec)) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt @@ -0,0 +1,18 @@ +package me.rhunk.snapenhance.core.download.data + +enum class MediaFilter( + val key: String, + val shouldIgnoreFilter: Boolean = false +) { + NONE("none", true), + PENDING("pending", true), + CHAT_MEDIA("chat_media"), + STORY("story"), + SPOTLIGHT("spotlight"), + PROFILE_PICTURE("profile_picture"); + + fun matches(source: String?): Boolean { + if (source == null) return false + return source.contains(key, ignoreCase = true) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/SplitMediaAssetType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/SplitMediaAssetType.kt @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.core.download.data + +enum class SplitMediaAssetType { + ORIGINAL, OVERLAY +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/EventBus.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.core.eventbus -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ModContext import kotlin.reflect.KClass @@ -14,7 +13,7 @@ interface IListener<T> { } class EventBus( - private val context: ModContext + val context: ModContext ) { private val subscribers = mutableMapOf<KClass<out Event>, MutableList<IListener<out Event>>>() @@ -34,7 +33,7 @@ class EventBus( runCatching { listener(event) }.onFailure { - Logger.error("Error while handling event ${event::class.simpleName}", it) + context.log.error("Error while handling event ${event::class.simpleName}", it) } } } @@ -61,7 +60,7 @@ class EventBus( runCatching { (listener as IListener<T>).handle(event) }.onFailure { t -> - Logger.error("Error while handling event ${event::class.simpleName} by ${listener::class.simpleName}", t) + context.log.error("Error while handling event ${event::class.simpleName} by ${listener::class.simpleName}", t) } } return event diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt @@ -1,253 +0,0 @@ -package me.rhunk.snapenhance.database - -import android.annotation.SuppressLint -import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.database.objects.* -import me.rhunk.snapenhance.manager.Manager -import java.io.File - -@SuppressLint("Range") -class DatabaseAccess(private val context: ModContext) : Manager { - private val databaseLock = Any() - - private val arroyoDatabase: File by lazy { - context.androidContext.getDatabasePath("arroyo.db") - } - - private val mainDatabase: File by lazy { - context.androidContext.getDatabasePath("main.db") - } - - private fun openMain(): SQLiteDatabase { - return SQLiteDatabase.openDatabase( - mainDatabase.absolutePath, - null, - SQLiteDatabase.OPEN_READONLY - )!! - } - - private fun openArroyo(): SQLiteDatabase { - return SQLiteDatabase.openDatabase( - arroyoDatabase.absolutePath, - null, - SQLiteDatabase.OPEN_READONLY - )!! - } - - fun hasArroyo(): Boolean { - return arroyoDatabase.exists() - } - - private fun <T> safeDatabaseOperation( - database: SQLiteDatabase, - query: (SQLiteDatabase) -> T? - ): T? { - synchronized(databaseLock) { - return runCatching { - query(database) - }.onFailure { - Logger.xposedLog("Database operation failed", it) - }.getOrNull() - } - } - - private fun <T : DatabaseObject> readDatabaseObject( - obj: T, - database: SQLiteDatabase, - table: String, - where: String, - args: Array<String> - ): T? { - val cursor = database.rawQuery("SELECT * FROM $table WHERE $where", args) - if (!cursor.moveToFirst()) { - cursor.close() - return null - } - try { - obj.write(cursor) - } catch (e: Throwable) { - Logger.xposedLog(e) - } - cursor.close() - return obj - } - - fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { - return safeDatabaseOperation(openMain()) { database -> - readDatabaseObject( - FriendFeedEntry(), - database, - "FriendsFeedView", - "friendUserId = ?", - arrayOf(userId) - ) - } - } - - val myUserId by lazy { - safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> - val cursor = arroyoDatabase.rawQuery(buildString { - append("SELECT * FROM required_values WHERE key = 'USERID'") - }, null) - - if (!cursor.moveToFirst()) { - cursor.close() - return@safeDatabaseOperation null - } - - val userId = cursor.getString(cursor.getColumnIndex("value")) - cursor.close() - userId - }!! - } - - fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { - return safeDatabaseOperation(openMain()) { - readDatabaseObject( - FriendFeedEntry(), - it, - "FriendsFeedView", - "key = ?", - arrayOf(conversationId) - ) - } - } - - fun getFriendInfo(userId: String): FriendInfo? { - return safeDatabaseOperation(openMain()) { - readDatabaseObject( - FriendInfo(), - it, - "FriendWithUsername", - "userId = ?", - arrayOf(userId) - ) - } - } - - fun getFeedEntries(limit: Int): List<FriendFeedEntry> { - return safeDatabaseOperation(openMain()) { database -> - val cursor = database.rawQuery( - "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", - arrayOf(limit.toString()) - ) - val list = mutableListOf<FriendFeedEntry>() - while (cursor.moveToNext()) { - val friendFeedEntry = FriendFeedEntry() - try { - friendFeedEntry.write(cursor) - } catch (_: Throwable) {} - list.add(friendFeedEntry) - } - cursor.close() - list - } ?: emptyList() - } - - fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { - return safeDatabaseOperation(openArroyo()) { - readDatabaseObject( - ConversationMessage(), - it, - "conversation_message", - "client_message_id = ?", - arrayOf(clientMessageId.toString()) - ) - } - } - - fun getConversationType(conversationId: String): Int? { - return safeDatabaseOperation(openArroyo()) { - val cursor = it.rawQuery( - "SELECT * FROM user_conversation WHERE client_conversation_id = ?", - arrayOf(conversationId) - ) - if (!cursor.moveToFirst()) { - cursor.close() - return@safeDatabaseOperation null - } - val type = cursor.getInt(cursor.getColumnIndex("conversation_type")) - cursor.close() - type - } - } - - fun getConversationLinkFromUserId(userId: String): UserConversationLink? { - return safeDatabaseOperation(openArroyo()) { - readDatabaseObject( - UserConversationLink(), - it, - "user_conversation", - "user_id = ? AND conversation_type = 0", - arrayOf(userId) - ) - } - } - - fun getDMOtherParticipant(conversationId: String): String? { - return safeDatabaseOperation(openArroyo()) { cursor -> - val query = cursor.rawQuery( - "SELECT * FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", - arrayOf(conversationId) - ) - val participants = mutableListOf<String>() - while (query.moveToNext()) { - participants.add(query.getString(query.getColumnIndex("user_id"))) - } - query.close() - participants.firstOrNull { it != myUserId } - } - } - - - fun getStoryEntryFromId(storyId: String): StoryEntry? { - return safeDatabaseOperation(openMain()) { - readDatabaseObject(StoryEntry(), it, "Story", "storyId = ?", arrayOf(storyId)) - } - } - - fun getConversationParticipants(conversationId: String): List<String>? { - return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> - val cursor = arroyoDatabase.rawQuery( - "SELECT * FROM user_conversation WHERE client_conversation_id = ?", - arrayOf(conversationId) - ) - if (!cursor.moveToFirst()) { - cursor.close() - return@safeDatabaseOperation emptyList() - } - val participants = mutableListOf<String>() - do { - participants.add(cursor.getString(cursor.getColumnIndex("user_id"))) - } while (cursor.moveToNext()) - cursor.close() - participants - } - } - - fun getMessagesFromConversationId( - conversationId: String, - limit: Int - ): List<ConversationMessage>? { - return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> - val cursor = arroyoDatabase.rawQuery( - "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", - arrayOf(conversationId, limit.toString()) - ) - if (!cursor.moveToFirst()) { - cursor.close() - return@safeDatabaseOperation emptyList() - } - val messages = mutableListOf<ConversationMessage>() - do { - val message = ConversationMessage() - message.write(cursor) - messages.add(message) - } while (cursor.moveToNext()) - cursor.close() - messages - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt @@ -1,7 +0,0 @@ -package me.rhunk.snapenhance.database - -import android.database.Cursor - -interface DatabaseObject { - fun write(cursor: Cursor) -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt @@ -1,50 +0,0 @@ -package me.rhunk.snapenhance.database.objects - -import android.annotation.SuppressLint -import android.database.Cursor -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.database.DatabaseObject -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( - var clientConversationId: String? = null, - var clientMessageId: Int = 0, - var serverMessageId: Int = 0, - var messageContent: ByteArray? = null, - var isSaved: Int = 0, - var isViewedByUser: Int = 0, - var contentType: Int = 0, - var creationTimestamp: Long = 0, - var readTimestamp: Long = 0, - var senderId: String? = null -) : DatabaseObject { - - @SuppressLint("Range") - override fun write(cursor: Cursor) { - with(cursor) { - clientConversationId = getStringOrNull("client_conversation_id") - clientMessageId = getInteger("client_message_id") - serverMessageId = getInteger("server_message_id") - messageContent = getBlobOrNull("message_content") - isSaved = getInteger("is_saved") - isViewedByUser = getInteger("is_viewed_by_user") - contentType = getInteger("content_type") - creationTimestamp = getLong("creation_timestamp") - readTimestamp = getLong("read_timestamp") - senderId = getStringOrNull("sender_id") - } - } - - fun getMessageAsString(): String? { - return when (ContentType.fromId(contentType)) { - ContentType.CHAT -> messageContent?.let { ProtoReader(it).getString(*Constants.ARROYO_STRING_CHAT_MESSAGE_PROTO) } - else -> null - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedEntry.kt @@ -1,47 +0,0 @@ -package me.rhunk.snapenhance.database.objects - -import android.annotation.SuppressLint -import android.database.Cursor -import me.rhunk.snapenhance.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 - -data class FriendFeedEntry( - var id: Int = 0, - var feedDisplayName: String? = null, - var participantsSize: Int = 0, - var lastInteractionTimestamp: Long = 0, - var displayTimestamp: Long = 0, - var displayInteractionType: String? = null, - var lastInteractionUserId: Int? = null, - var key: String? = null, - var friendUserId: String? = null, - var friendDisplayName: String? = null, - var friendDisplayUsername: String? = null, - var friendLinkType: Int? = null, - var bitmojiAvatarId: String? = null, - var bitmojiSelfieId: String? = null, -) : DatabaseObject { - - @SuppressLint("Range") - override fun write(cursor: Cursor) { - with(cursor) { - id = getInteger("_id") - feedDisplayName = getStringOrNull("feedDisplayName") - participantsSize = getInteger("participantsSize") - lastInteractionTimestamp = getLong("lastInteractionTimestamp") - displayTimestamp = getLong("displayTimestamp") - displayInteractionType = getStringOrNull("displayInteractionType") - lastInteractionUserId = getIntOrNull("lastInteractionUserId") - key = getStringOrNull("key") - friendUserId = getStringOrNull("friendUserId") - friendDisplayName = getStringOrNull("friendDisplayName") - friendDisplayUsername = getStringOrNull("friendDisplayUsername") - friendLinkType = getIntOrNull("friendLinkType") - bitmojiAvatarId = getStringOrNull("bitmojiAvatarId") - bitmojiSelfieId = getStringOrNull("bitmojiSelfieId") - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt @@ -1,64 +0,0 @@ -package me.rhunk.snapenhance.database.objects - -import android.annotation.SuppressLint -import android.database.Cursor -import me.rhunk.snapenhance.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 - -data class FriendInfo( - var id: Int = 0, - var lastModifiedTimestamp: Long = 0, - var username: String? = null, - var userId: String? = null, - var displayName: String? = null, - var bitmojiAvatarId: String? = null, - var bitmojiSelfieId: String? = null, - var bitmojiSceneId: String? = null, - var bitmojiBackgroundId: String? = null, - var friendmojis: String? = null, - var friendmojiCategories: String? = null, - var snapScore: Int = 0, - var birthday: Long = 0, - var addedTimestamp: Long = 0, - var reverseAddedTimestamp: Long = 0, - var serverDisplayName: String? = null, - var streakLength: Int = 0, - var streakExpirationTimestamp: Long = 0, - var reverseBestFriendRanking: Int = 0, - var isPinnedBestFriend: Int = 0, - var plusBadgeVisibility: Int = 0, - var usernameForSorting: String? = null -) : DatabaseObject, SerializableDataObject() { - @SuppressLint("Range") - override fun write(cursor: Cursor) { - with(cursor) { - id = getInteger("_id") - lastModifiedTimestamp = getLong("_lastModifiedTimestamp") - username = getStringOrNull("username") - userId = getStringOrNull("userId") - displayName = getStringOrNull("displayName") - bitmojiAvatarId = getStringOrNull("bitmojiAvatarId") - bitmojiSelfieId = getStringOrNull("bitmojiSelfieId") - bitmojiSceneId = getStringOrNull("bitmojiSceneId") - bitmojiBackgroundId = getStringOrNull("bitmojiBackgroundId") - friendmojis = getStringOrNull("friendmojis") - friendmojiCategories = getStringOrNull("friendmojiCategories") - snapScore = getInteger("score") - birthday = getLong("birthday") - addedTimestamp = getLong("addedTimestamp") - reverseAddedTimestamp = getLong("reverseAddedTimestamp") - serverDisplayName = getStringOrNull("serverDisplayName") - streakLength = getInteger("streakLength") - streakExpirationTimestamp = getLong("streakExpiration") - reverseBestFriendRanking = getInteger("reverseBestFriendRanking") - usernameForSorting = getStringOrNull("usernameForSorting") - if (getColumnIndex("isPinnedBestFriend") != -1) isPinnedBestFriend = - getInteger("isPinnedBestFriend") - if (getColumnIndex("plusBadgeVisibility") != -1) plusBadgeVisibility = - getInteger("plusBadgeVisibility") - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt @@ -1,27 +0,0 @@ -package me.rhunk.snapenhance.database.objects - -import android.annotation.SuppressLint -import android.database.Cursor -import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getStringOrNull - -data class StoryEntry( - var id: Int = 0, - var storyId: String? = null, - var displayName: String? = null, - var isLocal: Boolean? = null, - var userId: String? = null -) : DatabaseObject { - - @SuppressLint("Range") - override fun write(cursor: Cursor) { - with(cursor) { - id = getInteger("_id") - storyId = getStringOrNull("storyId") - displayName = getStringOrNull("displayName") - isLocal = getInteger("isLocal") == 1 - userId = getStringOrNull("userId") - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt @@ -1,23 +0,0 @@ -package me.rhunk.snapenhance.database.objects - -import android.annotation.SuppressLint -import android.database.Cursor -import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.ktx.getInteger -import me.rhunk.snapenhance.util.ktx.getStringOrNull - -class UserConversationLink( - var userId: String? = null, - var clientConversationId: String? = null, - var conversationType: Int = 0 -) : DatabaseObject { - - @SuppressLint("Range") - override fun write(cursor: Cursor) { - with(cursor) { - userId = getStringOrNull("user_id") - clientConversationId = getStringOrNull("client_conversation_id") - conversationType = getInteger("conversation_type") - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt @@ -1,69 +0,0 @@ -package me.rhunk.snapenhance.download - -import android.content.Intent -import android.os.Bundle -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.bridge.DownloadCallback -import me.rhunk.snapenhance.download.data.DashOptions -import me.rhunk.snapenhance.download.data.DownloadMediaType -import me.rhunk.snapenhance.download.data.DownloadMetadata -import me.rhunk.snapenhance.download.data.DownloadRequest -import me.rhunk.snapenhance.download.data.InputMedia -import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair - -class DownloadManagerClient ( - private val context: ModContext, - private val metadata: DownloadMetadata, - private val callback: DownloadCallback -) { - companion object { - const val DOWNLOAD_REQUEST_EXTRA = "request" - const val DOWNLOAD_METADATA_EXTRA = "metadata" - } - - private fun enqueueDownloadRequest(request: DownloadRequest) { - context.bridgeClient.enqueueDownload(Intent().apply { - putExtras(Bundle().apply { - putString(DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request)) - putString(DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata)) - }) - }, callback) - } - - fun downloadDashMedia(playlistUrl: String, offsetTime: Long, duration: Long?) { - enqueueDownloadRequest( - DownloadRequest( - inputMedias = arrayOf(InputMedia( - content = playlistUrl, - type = DownloadMediaType.REMOTE_MEDIA - )), - dashOptions = DashOptions(offsetTime, duration), - flags = DownloadRequest.Flags.IS_DASH_PLAYLIST - ) - ) - } - - fun downloadSingleMedia(mediaData: String, mediaType: DownloadMediaType, encryption: MediaEncryptionKeyPair? = null) { - enqueueDownloadRequest( - DownloadRequest( - inputMedias = arrayOf(InputMedia( - content = mediaData, - type = mediaType, - encryption = encryption - )) - ) - ) - } - - fun downloadMediaWithOverlay( - original: InputMedia, - overlay: InputMedia, - ) { - enqueueDownloadRequest( - DownloadRequest( - inputMedias = arrayOf(original, overlay), - flags = DownloadRequest.Flags.MERGE_OVERLAY - ) - ) - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt @@ -1,182 +0,0 @@ -package me.rhunk.snapenhance.download - -import android.annotation.SuppressLint -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import me.rhunk.snapenhance.download.data.DownloadMetadata -import me.rhunk.snapenhance.download.data.DownloadObject -import me.rhunk.snapenhance.download.data.DownloadStage -import me.rhunk.snapenhance.download.data.MediaFilter -import me.rhunk.snapenhance.util.SQLiteDatabaseHelper -import me.rhunk.snapenhance.util.ktx.getIntOrNull -import me.rhunk.snapenhance.util.ktx.getStringOrNull - -class DownloadTaskManager { - private lateinit var taskDatabase: SQLiteDatabase - private val pendingTasks = mutableMapOf<Int, DownloadObject>() - private val cachedTasks = mutableMapOf<Int, DownloadObject>() - - @SuppressLint("Range") - fun init(context: Context) { - if (this::taskDatabase.isInitialized) return - taskDatabase = context.openOrCreateDatabase("download_tasks", Context.MODE_PRIVATE, null).apply { - SQLiteDatabaseHelper.createTablesFromSchema(this, mapOf( - "tasks" to listOf( - "id INTEGER PRIMARY KEY AUTOINCREMENT", - "hash VARCHAR UNIQUE", - "outputPath TEXT", - "outputFile TEXT", - "mediaDisplayType TEXT", - "mediaDisplaySource TEXT", - "iconUrl TEXT", - "downloadStage TEXT" - ) - )) - } - } - - fun addTask(task: DownloadObject): Int { - taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)", - arrayOf( - task.metadata.mediaIdentifier, - task.metadata.outputPath, - task.outputFile, - task.metadata.mediaDisplayType, - task.metadata.mediaDisplaySource, - task.metadata.iconUrl, - task.downloadStage.name - ) - ) - task.downloadId = taskDatabase.rawQuery("SELECT last_insert_rowid()", null).use { - it.moveToFirst() - it.getInt(0) - } - pendingTasks[task.downloadId] = task - return task.downloadId - } - - fun updateTask(task: DownloadObject) { - taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?", - arrayOf( - task.metadata.mediaIdentifier, - task.metadata.outputPath, - task.outputFile, - task.metadata.mediaDisplayType, - task.metadata.mediaDisplaySource, - task.metadata.iconUrl, - task.downloadStage.name, - task.downloadId - ) - ) - //if the task is no longer active, move it to the cached tasks - if (task.isJobActive()) { - pendingTasks[task.downloadId] = task - } else { - pendingTasks.remove(task.downloadId) - cachedTasks[task.downloadId] = task - } - } - - @SuppressLint("Range") - fun canDownloadMedia(mediaIdentifier: String?): DownloadStage? { - if (mediaIdentifier == null) return null - - val cursor = taskDatabase.rawQuery("SELECT * FROM tasks WHERE hash = ?", arrayOf(mediaIdentifier)) - if (cursor.count > 0) { - cursor.moveToFirst() - val downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage"))) - cursor.close() - - //if the stage has reached a final stage and is not in a saved state, remove the task - if (downloadStage.isFinalStage && downloadStage != DownloadStage.SAVED) { - taskDatabase.execSQL("DELETE FROM tasks WHERE hash = ?", arrayOf(mediaIdentifier)) - return null - } - - return downloadStage - } - cursor.close() - return null - } - - fun isEmpty(): Boolean { - return cachedTasks.isEmpty() && pendingTasks.isEmpty() - } - - private fun removeTask(id: Int) { - taskDatabase.execSQL("DELETE FROM tasks WHERE id = ?", arrayOf(id)) - cachedTasks.remove(id) - pendingTasks.remove(id) - } - - fun removeTask(task: DownloadObject) { - removeTask(task.downloadId) - } - - fun queryFirstTasks(filter: MediaFilter): Map<Int, DownloadObject> { - val isPendingFilter = filter == MediaFilter.PENDING - val tasks = mutableMapOf<Int, DownloadObject>() - - tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.mediaDisplayType) }) - if (isPendingFilter) { - return tasks.toSortedMap(reverseOrder()) - } - - tasks.putAll(queryTasks( - from = tasks.values.lastOrNull()?.downloadId ?: Int.MAX_VALUE, - amount = 30, - filter = filter - )) - - return tasks.toSortedMap(reverseOrder()) - } - - @SuppressLint("Range") - fun queryTasks(from: Int, amount: Int = 30, filter: MediaFilter = MediaFilter.NONE): Map<Int, DownloadObject> { - if (filter == MediaFilter.PENDING) { - return emptyMap() - } - - val cursor = taskDatabase.rawQuery( - "SELECT * FROM tasks WHERE id < ? AND mediaDisplayType LIKE ? ORDER BY id DESC LIMIT ?", - arrayOf( - from.toString(), - if (filter.shouldIgnoreFilter) "%" else "%${filter.key}", - amount.toString() - ) - ) - - val result = sortedMapOf<Int, DownloadObject>() - - while (cursor.moveToNext()) { - val task = DownloadObject( - downloadId = cursor.getIntOrNull("id")!!, - outputFile = cursor.getStringOrNull("outputFile"), - metadata = DownloadMetadata( - outputPath = cursor.getStringOrNull("outputPath")!!, - mediaIdentifier = cursor.getStringOrNull("hash"), - mediaDisplayType = cursor.getStringOrNull("mediaDisplayType"), - mediaDisplaySource = cursor.getStringOrNull("mediaDisplaySource"), - iconUrl = cursor.getStringOrNull("iconUrl") - ) - ).apply { - downloadTaskManager = this@DownloadTaskManager - downloadStage = DownloadStage.valueOf(cursor.getStringOrNull("downloadStage")!!) - //if downloadStage is not saved, it means the app was killed before the download was finished - if (downloadStage != DownloadStage.SAVED) { - downloadStage = DownloadStage.FAILED - } - } - result[task.downloadId] = task - } - cursor.close() - - return result.toSortedMap(reverseOrder()) - } - - fun removeAllTasks() { - taskDatabase.execSQL("DELETE FROM tasks") - cachedTasks.clear() - pendingTasks.clear() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMediaType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMediaType.kt @@ -1,22 +0,0 @@ -package me.rhunk.snapenhance.download.data - -import android.net.Uri - -enum class DownloadMediaType { - PROTO_MEDIA, - DIRECT_MEDIA, - REMOTE_MEDIA, - LOCAL_MEDIA; - - companion object { - fun fromUri(uri: Uri): DownloadMediaType { - return when (uri.scheme) { - "proto" -> PROTO_MEDIA - "direct" -> DIRECT_MEDIA - "http", "https" -> REMOTE_MEDIA - "file" -> LOCAL_MEDIA - else -> throw IllegalArgumentException("Unknown uri scheme: ${uri.scheme}") - } - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt @@ -1,9 +0,0 @@ -package me.rhunk.snapenhance.download.data - -data class DownloadMetadata( - val mediaIdentifier: String?, - val outputPath: String, - val mediaDisplaySource: String?, - val mediaDisplayType: String?, - val iconUrl: String? -)- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadObject.kt @@ -1,32 +0,0 @@ -package me.rhunk.snapenhance.download.data - -import kotlinx.coroutines.Job -import me.rhunk.snapenhance.download.DownloadTaskManager - -data class DownloadObject( - var downloadId: Int = 0, - var outputFile: String? = null, - val metadata : DownloadMetadata -) { - lateinit var downloadTaskManager: DownloadTaskManager - var job: Job? = null - - var changeListener = { _: DownloadStage, _: DownloadStage -> } - private var _stage: DownloadStage = DownloadStage.PENDING - var downloadStage: DownloadStage - get() = synchronized(this) { - _stage - } - set(value) = synchronized(this) { - changeListener(_stage, value) - _stage = value - downloadTaskManager.updateTask(this) - } - - fun isJobActive() = job?.isActive == true - - fun cancel() { - downloadStage = DownloadStage.CANCELLED - job?.cancel() - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt @@ -1,26 +0,0 @@ -package me.rhunk.snapenhance.download.data - - -data class DashOptions(val offsetTime: Long, val duration: Long?) -data class InputMedia( - val content: String, - val type: DownloadMediaType, - val encryption: MediaEncryptionKeyPair? = null -) - -class DownloadRequest( - val inputMedias: Array<InputMedia>, - val dashOptions: DashOptions? = null, - private val flags: Int = 0, -) { - object Flags { - const val MERGE_OVERLAY = 1 - const val IS_DASH_PLAYLIST = 2 - } - - val isDashPlaylist: Boolean - get() = flags and Flags.IS_DASH_PLAYLIST != 0 - - val shouldMergeOverlay: Boolean - get() = flags and Flags.MERGE_OVERLAY != 0 -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadStage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadStage.kt @@ -1,14 +0,0 @@ -package me.rhunk.snapenhance.download.data - -enum class DownloadStage( - val isFinalStage: Boolean = false, -) { - PENDING(false), - DOWNLOADING(false), - MERGING(false), - DOWNLOADED(true), - SAVED(true), - MERGE_FAILED(true), - FAILED(true), - CANCELLED(true) -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt @@ -1,21 +0,0 @@ -@file:OptIn(ExperimentalEncodingApi::class) - -package me.rhunk.snapenhance.download.data - -import me.rhunk.snapenhance.data.wrapper.impl.media.EncryptionWrapper -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -// key and iv are base64 encoded -data class MediaEncryptionKeyPair( - val key: String, - val iv: String -) - -fun Pair<ByteArray, ByteArray>.toKeyPair(): MediaEncryptionKeyPair { - return MediaEncryptionKeyPair(Base64.UrlSafe.encode(this.first), Base64.UrlSafe.encode(this.second)) -} - -fun EncryptionWrapper.toKeyPair(): MediaEncryptionKeyPair { - return MediaEncryptionKeyPair(Base64.UrlSafe.encode(this.keySpec), Base64.UrlSafe.encode(this.ivKeyParameterSpec)) -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaFilter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaFilter.kt @@ -1,18 +0,0 @@ -package me.rhunk.snapenhance.download.data - -enum class MediaFilter( - val key: String, - val shouldIgnoreFilter: Boolean = false -) { - NONE("none", true), - PENDING("pending", true), - CHAT_MEDIA("chat_media"), - STORY("story"), - SPOTLIGHT("spotlight"), - PROFILE_PICTURE("profile_picture"); - - fun matches(source: String?): Boolean { - if (source == null) return false - return source.contains(key, ignoreCase = true) - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt @@ -1,5 +0,0 @@ -package me.rhunk.snapenhance.download.data - -enum class SplitMediaAssetType { - ORIGINAL, OVERLAY -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features -import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType import java.io.BufferedReader import java.io.ByteArrayInputStream import java.io.InputStreamReader diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt @@ -10,7 +10,6 @@ import android.net.Uri import android.os.Build import android.os.Environment import com.google.gson.JsonParser -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams @@ -36,7 +35,7 @@ class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVI runCatching { checkForUpdates() }.onFailure { - Logger.error("Failed to check for updates: ${it.message}", it) + context.log.error("Failed to check for updates: ${it.message}", it) }.onSuccess { context.bridgeClient.setAutoUpdaterTime(currentTimeMillis) } 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 @@ -6,9 +6,15 @@ import android.graphics.BitmapFactory import android.net.Uri import android.widget.ImageView import kotlinx.coroutines.runBlocking -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.Logger.xposedLog import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.core.database.objects.FriendInfo +import me.rhunk.snapenhance.core.download.DownloadManagerClient +import me.rhunk.snapenhance.core.download.data.DownloadMediaType +import me.rhunk.snapenhance.core.download.data.DownloadMetadata +import me.rhunk.snapenhance.core.download.data.InputMedia +import me.rhunk.snapenhance.core.download.data.MediaFilter +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.data.ContentType import me.rhunk.snapenhance.data.FileType @@ -17,14 +23,6 @@ import me.rhunk.snapenhance.data.wrapper.impl.media.dash.LongformVideoPlaylistIt import me.rhunk.snapenhance.data.wrapper.impl.media.dash.SnapPlaylistItem import me.rhunk.snapenhance.data.wrapper.impl.media.opera.Layer import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap -import me.rhunk.snapenhance.database.objects.FriendInfo -import me.rhunk.snapenhance.download.DownloadManagerClient -import me.rhunk.snapenhance.download.data.DownloadMediaType -import me.rhunk.snapenhance.download.data.DownloadMetadata -import me.rhunk.snapenhance.download.data.InputMedia -import me.rhunk.snapenhance.download.data.MediaFilter -import me.rhunk.snapenhance.download.data.SplitMediaAssetType -import me.rhunk.snapenhance.download.data.toKeyPair import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging @@ -84,19 +82,19 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp callback = object: DownloadCallback.Stub() { override fun onSuccess(outputFile: String) { if (!downloadLogging.contains("success")) return - Logger.debug("onSuccess: outputFile=$outputFile") + context.log.verbose("onSuccess: outputFile=$outputFile") context.shortToast(context.translation.format("download_processor.saved_toast", "path" to outputFile.split("/").takeLast(2).joinToString("/"))) } override fun onProgress(message: String) { if (!downloadLogging.contains("progress")) return - Logger.debug("onProgress: message=$message") + context.log.verbose("onProgress: message=$message") context.shortToast(message) } override fun onFailure(message: String, throwable: String?) { if (!downloadLogging.contains("failure")) return - Logger.debug("onFailure: message=$message, throwable=$throwable") + context.log.verbose("onFailure: message=$message, throwable=$throwable") throwable?.let { context.longToast((message + it.takeIf { it.isNotEmpty() }.orEmpty())) return @@ -402,8 +400,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp try { handleOperaMedia(mediaParamMap, mediaInfoMap, false) } catch (e: Throwable) { - xposedLog(e) - context.longToast(e.message!!) + context.log.error("Failed to handle opera media", e) + context.longToast(e.message) } } } @@ -524,11 +522,11 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } }.onFailure { context.shortToast(translations["failed_to_create_preview_toast"]) - xposedLog(it) + context.log.error("Failed to create preview", it) } }.onFailure { context.longToast(translations["failed_generic_toast"]) - xposedLog(it) + context.log.error("Failed to download message", it) } } 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 @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.downloader import android.annotation.SuppressLint import android.widget.Button import android.widget.RelativeLayout -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.features.Feature @@ -45,7 +44,7 @@ class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams friendUsername!! ) }.onFailure { - Logger.error("Failed to download profile picture", it) + this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it) } } }.show() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.features.impl.experiments -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage @@ -17,11 +16,11 @@ class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParam val fingerprintClass = android.os.Build::class.java Hooker.hook(fingerprintClass, "FINGERPRINT", HookStage.BEFORE) { hookAdapter -> hookAdapter.setResult(fingerprint) - Logger.debug("Fingerprint spoofed to $fingerprint") + context.log.verbose("Fingerprint spoofed to $fingerprint") } Hooker.hook(fingerprintClass, "deriveFingerprint", HookStage.BEFORE) { hookAdapter -> hookAdapter.setResult(fingerprint) - Logger.debug("Fingerprint spoofed to $fingerprint") + context.log.verbose("Fingerprint spoofed to $fingerprint") } } @@ -30,7 +29,7 @@ class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParam Hooker.hook(settingsSecureClass, "getString", HookStage.BEFORE) { hookAdapter -> if(hookAdapter.args()[1] == "android_id") { hookAdapter.setResult(androidId) - Logger.debug("Android ID spoofed to $androidId") + context.log.verbose("Android ID spoofed to $androidId") } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.features.impl.privacy -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.data.NotificationType import me.rhunk.snapenhance.features.Feature @@ -28,7 +27,7 @@ class PreventMessageSending : Feature("Prevent message sending", loadParams = Fe val associatedType = NotificationType.fromContentType(contentType) ?: return@subscribe if (preventMessageSending.contains(associatedType.key)) { - Logger.debug("Preventing message sending for $associatedType") + context.log.verbose("Preventing message sending for $associatedType") event.canceled = true } } 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 @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.features.impl.spying import kotlinx.coroutines.runBlocking -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.spying import android.os.DeadObjectException import com.google.gson.JsonObject import com.google.gson.JsonParser -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MessageState import me.rhunk.snapenhance.data.wrapper.impl.Message @@ -72,7 +71,7 @@ class MessageLogger : Feature("MessageLogger", context.database.getFeedEntries(PREFETCH_FEED_COUNT).forEach { friendFeedInfo -> fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList()) } - }.also { Logger.debug("Loaded ${fetchedMessages.size} cached messages in $it") } + }.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in $it") } } private fun processSnapMessage(messageInstance: Any) { 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 @@ -41,7 +41,7 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, val callback = CallbackBuilder(callbackClass) .override("onError") { - Logger.xposedLog("Error saving message $messageId") + context.log.warn("Error saving message $messageId") }.build() runCatching { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.tweaks import android.os.Build import android.os.FileObserver import com.google.gson.JsonParser -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams @@ -32,7 +31,7 @@ class DisableVideoLengthRestriction : Feature("DisableVideoLengthRestriction", l val fileContent = JsonParser.parseReader(file.reader()).asJsonObject if (fileContent["timerOrDuration"].asLong < 0) file.delete() }.onFailure { - Logger.error("Failed to read story metadata file", it) + context.log.error("Failed to read story metadata file", it) } } }) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.features.impl.tweaks import android.app.AlertDialog -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage @@ -15,7 +14,7 @@ class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs", loadParams = Fe findClass("com.google.android.gms.common.GoogleApiAvailability").methods .first { Modifier.isStatic(it.modifiers) && it.returnType == AlertDialog::class.java }.let { method -> method.hook(HookStage.BEFORE) { param -> - Logger.debug("GoogleApiAvailability.showErrorDialogFragment() called, returning null") + context.log.verbose("GoogleApiAvailability.showErrorDialogFragment() called, returning null") param.setResult(null) } } 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 @@ -13,12 +13,12 @@ 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.download.data.SplitMediaAssetType import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.data.ContentType 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.download.data.SplitMediaAssetType import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.Messaging @@ -297,7 +297,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN fetchMessagesResult(conversationId, messageList) } .override("onError") { - Logger.xposedLog("Failed to fetch message ${it.arg(0) as Any}") + context.log.error("Failed to fetch message ${it.arg(0) as Any}") }.build() fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instanceNonNull(), callback) @@ -323,7 +323,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val intent = param.argNullable<Intent>(0) ?: return@hook val messageType = intent.getStringExtra("type") ?: return@hook - Logger.xposedLog("received message type: $messageType") + context.log.debug("received message type: $messageType") if (states.contains(messageType.replaceFirst("mischief_", ""))) { param.setResult(null) 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,6 +1,6 @@ package me.rhunk.snapenhance.features.impl.ui -import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.bridge.types.BridgeFileType import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt @@ -1,38 +1,37 @@ package me.rhunk.snapenhance.manager.impl +import android.content.Intent import me.rhunk.snapenhance.ModContext import me.rhunk.snapenhance.action.AbstractAction -import me.rhunk.snapenhance.action.impl.CheckForUpdates -import me.rhunk.snapenhance.action.impl.CleanCache -import me.rhunk.snapenhance.action.impl.ClearMessageLogger -import me.rhunk.snapenhance.action.impl.ExportChatMessages -import me.rhunk.snapenhance.action.impl.OpenMap -import me.rhunk.snapenhance.action.impl.RefreshMappings -import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.action.EnumAction import me.rhunk.snapenhance.manager.Manager -import kotlin.reflect.KClass class ActionManager( - private val context: ModContext, + private val modContext: ModContext, ) : Manager { - private val actions = mutableMapOf<String, AbstractAction>() - fun getActions() = actions.values.toList() - private fun load(clazz: KClass<out AbstractAction>) { - val action = clazz.java.newInstance() - action.context = context - actions[action.nameKey] = action + companion object { + const val ACTION_PARAMETER = "se_action" } + private val actions = mutableMapOf<String, AbstractAction>() + override fun init() { - load(CleanCache::class) - load(ExportChatMessages::class) - load(OpenMap::class) - load(CheckForUpdates::class) - if(BuildConfig.DEBUG) { - load(ClearMessageLogger::class) - load(RefreshMappings::class) + EnumAction.values().forEach { enumAction -> + actions[enumAction.key] = enumAction.clazz.java.getConstructor().newInstance().apply { + this.context = modContext + } } + } + fun onNewIntent(intent: Intent?) { + val action = intent?.getStringExtra(ACTION_PARAMETER) ?: return + execute(EnumAction.values().find { it.key == action } ?: return) + intent.removeExtra(ACTION_PARAMETER) + } - actions.values.forEach(AbstractAction::init) + private fun execute(action: EnumAction) { + actions[action.key]?.run() + if (action.exitOnFinish) { + modContext.forceCloseApp() + } } } \ No newline at end of file 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 @@ -11,11 +11,10 @@ import android.view.View import android.widget.Button import android.widget.CompoundButton import android.widget.Switch -import me.rhunk.snapenhance.Logger +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.data.ContentType -import me.rhunk.snapenhance.database.objects.ConversationMessage -import me.rhunk.snapenhance.database.objects.FriendInfo -import me.rhunk.snapenhance.database.objects.UserConversationLink import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader @@ -57,7 +56,7 @@ class FriendFeedInfoMenu : AbstractMenu() { ) } } catch (e: Throwable) { - Logger.xposedLog(e) + context.log.error("Error loading bitmoji selfie", e) } val finalIcon = icon context.runOnUiThread { @@ -243,7 +242,6 @@ class FriendFeedInfoMenu : AbstractMenu() { rules.forEach { ruleFeature -> if (!friendFeedMenuOptions.contains(ruleFeature.ruleType.key)) return@forEach - Logger.debug("${ruleFeature.ruleType.key} ${ruleFeature.getRuleState()}") val ruleState = ruleFeature.getRuleState() ?: return@forEach createToggleFeature(viewConsumer, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt @@ -8,7 +8,6 @@ import android.widget.Button import android.widget.LinearLayout import android.widget.ScrollView import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.ui.ViewAppearanceHelper.applyTheme import me.rhunk.snapenhance.ui.menu.AbstractMenu @@ -76,7 +75,7 @@ class OperaContextActionMenu : AbstractMenu() { linearLayout.addView(button) (childView as ViewGroup).addView(linearLayout, 0) } catch (e: Throwable) { - Logger.xposedLog(e) + context.log.error("Error while injecting OperaContextActionMenu", e) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt @@ -2,15 +2,13 @@ package me.rhunk.snapenhance.ui.menu.impl import android.annotation.SuppressLint import android.view.View -import android.widget.Button -import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.menu.AbstractMenu class SettingsMenu : AbstractMenu() { //TODO: quick settings @SuppressLint("SetTextI18n") fun inject(viewModel: View, addView: (View) -> Unit) { - val actions = context.actionManager.getActions().map { + /*val actions = context.actionManager.getActions().map { Pair(it) { val button = Button(viewModel.context) button.text = context.translation[it.nameKey] @@ -25,6 +23,6 @@ class SettingsMenu : AbstractMenu() { actions.forEach { addView(it.second()) - } + }*/ } } \ 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 @@ -23,7 +23,7 @@ object SQLiteDatabaseHelper { if (newColumns.isEmpty()) return@forEach - Logger.log("Schema for table $tableName has changed") + Logger.directDebug("Schema for table $tableName has changed") sqLiteDatabase.execSQL("DROP TABLE $tableName") sqLiteDatabase.execSQL("CREATE TABLE IF NOT EXISTS $tableName (${columns.joinToString(", ")})") } 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 @@ -37,7 +37,7 @@ class HttpServer( } coroutineScope.launch(Dispatchers.IO) { - Logger.debug("starting http server on port $port") + Logger.directDebug("starting http server on port $port") serverSocket = ServerSocket(port) callback(this@HttpServer) while (!serverSocket!!.isClosed) { @@ -48,21 +48,21 @@ class HttpServer( handleRequest(socket) timeoutJob = launch { delay(timeout.toLong()) - Logger.debug("http server closed due to timeout") + Logger.directDebug("http server closed due to timeout") runCatching { socketJob?.cancel() socket.close() serverSocket?.close() }.onFailure { - Logger.error(it) + Logger.directError("failed to close socket", it) } } } } catch (e: SocketException) { - Logger.debug("http server timed out") + Logger.directDebug("http server timed out") break; } catch (e: Throwable) { - Logger.error("failed to handle request", e) + Logger.directError("failed to handle request", e) } } }.also { socketJob = it } @@ -90,13 +90,13 @@ class HttpServer( outputStream.close() socket.close() }.onFailure { - Logger.error("failed to close socket", it) + 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.debug("[http-server:${port}] $method $fileRequested") + Logger.directDebug("[http-server:${port}] $method $fileRequested") if (method != "GET") { with(writer) { 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 @@ -44,7 +44,7 @@ object RemoteMediaResolver { okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { - Logger.log("Unexpected code $response") + 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 @@ -9,16 +9,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.Logger 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.database.objects.FriendFeedEntry -import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.EncryptionHelper import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper @@ -138,7 +137,7 @@ class MessageExporter( } }.onFailure { printLog("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}") - Logger.error("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}", it) + context.log.error("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}", it) } } } @@ -219,7 +218,7 @@ class MessageExporter( } }.onFailure { printLog("failed to read template from apk") - Logger.error("failed to read template from apk", it) + context.log.error("failed to read template from apk", it) } output.write("</html>".toByteArray()) 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 @@ -4,9 +4,9 @@ import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegSession import kotlinx.coroutines.suspendCancellableCoroutine 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.download.data.SplitMediaAssetType import me.rhunk.snapenhance.util.download.RemoteMediaResolver import me.rhunk.snapenhance.util.protobuf.ProtoReader import java.io.ByteArrayInputStream