feat: log system

- debug actions
- move packages to core
This commit is contained in:
rhunk 2023-08-31 00:59:30 +02:00
parent 6b9e44700d
commit 61da95f41b
83 changed files with 1118 additions and 515 deletions

View File

@ -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())
}
}
}

View File

@ -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
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"
)
},
mappingsInfo = if (mappings.isMappingsLoaded()) {
ModMappingsInfo(
generatedSnapchatVersion = mappings.getGeneratedBuildNumber(),
isOutdated = mappings.isMappingsOutdated()
)
} 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 {

View File

@ -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) }

View File

@ -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)
}
}

View File

@ -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

View File

@ -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() {}

View File

@ -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?,
)

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}
}) {

View File

@ -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)
}
}
}

View File

@ -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}!")
}
}
}
}
}
}

View File

@ -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)

View File

@ -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 },
) {

View File

@ -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)
}
}
}) {

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -5,53 +5,47 @@ import me.rhunk.snapenhance.bridge.DownloadCallback;
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);
}

View File

@ -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",

View File

@ -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

View File

@ -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 fromShortName(shortName: String): LogLevel? {
return values().find { it.shortName == shortName }
}
}
}
class Logger(
private val bridgeClient: BridgeClient
) {
companion object {
private const val TAG = "SnapEnhanceCore"
fun directDebug(message: Any?, tag: String = TAG) {
Log.println(Log.DEBUG, tag, message.toString())
}
fun directError(message: Any?, throwable: Throwable, tag: String = TAG) {
Log.println(Log.ERROR, tag, message.toString())
Log.println(Log.ERROR, tag, throwable.toString())
}
fun xposedLog(message: Any?, tag: String = TAG) {
Log.println(Log.INFO, tag, message.toString())
XposedBridge.log("$tag: $message")
}
fun xposedLog(message: Any?, throwable: Throwable, tag: String = TAG) {
Log.println(Log.INFO, tag, message.toString())
XposedBridge.log("$tag: $message")
XposedBridge.log(throwable)
}
}
fun debug(message: Any?) {
if (!BuildConfig.DEBUG) return
Log.d(TAG, message.toString())
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 debug(tag: String, message: Any?) {
if (!BuildConfig.DEBUG) return
Log.d(tag, message.toString())
fun debug(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.DEBUG, message)
fun error(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ERROR, message)
fun error(message: Any?, throwable: Throwable, tag: String = TAG) {
internalLog(tag, LogLevel.ERROR, message)
internalLog(tag, LogLevel.ERROR, throwable)
}
fun error(throwable: Throwable) {
Log.e(TAG, "", throwable)
}
fun info(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.INFO, message)
fun error(message: Any?) {
Log.e(TAG, message.toString())
}
fun verbose(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.VERBOSE, message)
fun error(message: Any?, throwable: Throwable) {
Log.e(TAG, message.toString(), throwable)
}
fun warn(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.WARN, message)
fun xposedLog(message: Any?) {
XposedBridge.log(message.toString())
}
fun xposedLog(message: Any?, throwable: Throwable?) {
XposedBridge.log(message.toString())
XposedBridge.log(throwable)
}
fun xposedLog(throwable: Throwable) {
XposedBridge.log(throwable)
}
fun assert(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ASSERT, message)
}

View File

@ -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(

View File

@ -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()

View File

@ -3,16 +3,9 @@ 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
*/

View File

@ -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);
}

View File

@ -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")
}
}
}
}

View File

@ -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")

View File

@ -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")
}
}

View File

@ -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!!)

View File

@ -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()

View File

@ -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()
}
}

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.bridge
package me.rhunk.snapenhance.core.bridge
import android.content.ComponentName
@ -10,11 +10,13 @@ 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.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
@ -29,7 +31,7 @@ class BridgeClient(
private lateinit var service: BridgeInterface
companion object {
const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.bridge.SYNC"
const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.core.bridge.SYNC"
}
fun start(callback: (Boolean) -> Unit) {
@ -76,7 +78,7 @@ class BridgeClient(
}
override fun onNullBinding(name: ComponentName) {
xposedLog("failed to connect to bridge service")
context.log.error("BridgeClient", "failed to connect to bridge service")
exitProcess(1)
}
@ -84,6 +86,8 @@ class BridgeClient(
exitProcess(0)
}
fun broadcastLog(tag: String, level: String, message: String) = service.broadcastLog(tag, level, message)
fun createAndReadFile(
fileType: BridgeFileType,
defaultContent: ByteArray
@ -108,8 +112,6 @@ class BridgeClient(
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)
}

View File

@ -1,7 +1,7 @@
package me.rhunk.snapenhance.bridge
package me.rhunk.snapenhance.core.bridge
import android.content.Context
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.core.bridge.types.BridgeFileType
open class FileLoaderWrapper(
private val fileType: BridgeFileType,

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.bridge.types
package me.rhunk.snapenhance.core.bridge.types
import android.content.Context
import java.io.File

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.bridge.types
package me.rhunk.snapenhance.core.bridge.types
enum class FileActionType {
CREATE_AND_READ, READ, WRITE, DELETE, EXISTS

View File

@ -1,10 +1,10 @@
package me.rhunk.snapenhance.bridge.wrapper
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.bridge.BridgeClient
import me.rhunk.snapenhance.core.bridge.BridgeClient
import me.rhunk.snapenhance.data.LocalePair
import java.util.Locale
@ -80,7 +80,7 @@ class LocaleWrapper {
loadFromContext(context)
}
operator fun get(key: String) = translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") }
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 ->

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.bridge.wrapper
package me.rhunk.snapenhance.core.bridge.wrapper
import android.content.Context
import com.google.gson.GsonBuilder
@ -6,8 +6,8 @@ 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.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
@ -58,11 +58,8 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
runCatching {
loadCached()
}.onFailure {
Logger.error("Failed to load cached mappings", it)
delete()
}
} else {
Logger.debug("Mappings file does not exist")
}
}
@ -122,7 +119,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
}
write(result.toString().toByteArray())
}.also {
Logger.debug("Generated mappings in $it ms")
Logger.directDebug("Generated mappings in $it ms")
}
}

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.bridge.wrapper
package me.rhunk.snapenhance.core.bridge.wrapper
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase

View File

@ -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

View File

@ -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()
}
}

View File

@ -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")
}

View File

@ -1,10 +1,14 @@
package me.rhunk.snapenhance.database
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.database.objects.*
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
@ -68,7 +72,7 @@ class DatabaseAccess(private val context: ModContext) : Manager {
try {
obj.write(cursor)
} catch (e: Throwable) {
Logger.xposedLog(e)
context.log.error("Failed to read database object", e)
}
cursor.close()
return obj

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.database
package me.rhunk.snapenhance.core.database
import android.database.Cursor

View File

@ -1,10 +1,10 @@
package me.rhunk.snapenhance.database.objects
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.database.DatabaseObject
import me.rhunk.snapenhance.util.ktx.getBlobOrNull
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getLong

View File

@ -1,8 +1,8 @@
package me.rhunk.snapenhance.database.objects
package me.rhunk.snapenhance.core.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
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

View File

@ -1,8 +1,8 @@
package me.rhunk.snapenhance.database.objects
package me.rhunk.snapenhance.core.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
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

View File

@ -1,8 +1,8 @@
package me.rhunk.snapenhance.database.objects
package me.rhunk.snapenhance.core.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.core.database.DatabaseObject
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getStringOrNull

View File

@ -1,8 +1,8 @@
package me.rhunk.snapenhance.database.objects
package me.rhunk.snapenhance.core.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.core.database.DatabaseObject
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getStringOrNull

View File

@ -1,15 +1,15 @@
package me.rhunk.snapenhance.download
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.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
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,
@ -33,10 +33,12 @@ class DownloadManagerClient (
fun downloadDashMedia(playlistUrl: String, offsetTime: Long, duration: Long?) {
enqueueDownloadRequest(
DownloadRequest(
inputMedias = arrayOf(InputMedia(
inputMedias = arrayOf(
InputMedia(
content = playlistUrl,
type = DownloadMediaType.REMOTE_MEDIA
)),
)
),
dashOptions = DashOptions(offsetTime, duration),
flags = DownloadRequest.Flags.IS_DASH_PLAYLIST
)
@ -46,11 +48,13 @@ class DownloadManagerClient (
fun downloadSingleMedia(mediaData: String, mediaType: DownloadMediaType, encryption: MediaEncryptionKeyPair? = null) {
enqueueDownloadRequest(
DownloadRequest(
inputMedias = arrayOf(InputMedia(
inputMedias = arrayOf(
InputMedia(
content = mediaData,
type = mediaType,
encryption = encryption
))
)
)
)
)
}

View File

@ -1,12 +1,12 @@
package me.rhunk.snapenhance.download
package me.rhunk.snapenhance.core.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.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

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
import android.net.Uri

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
data class DownloadMetadata(
val mediaIdentifier: String?,

View File

@ -1,7 +1,7 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
import kotlinx.coroutines.Job
import me.rhunk.snapenhance.download.DownloadTaskManager
import me.rhunk.snapenhance.core.download.DownloadTaskManager
data class DownloadObject(
var downloadId: Int = 0,

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
data class DashOptions(val offsetTime: Long, val duration: Long?)

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
enum class DownloadStage(
val isFinalStage: Boolean = false,

View File

@ -1,6 +1,6 @@
@file:OptIn(ExperimentalEncodingApi::class)
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
import me.rhunk.snapenhance.data.wrapper.impl.media.EncryptionWrapper
import kotlin.io.encoding.Base64

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
enum class MediaFilter(
val key: String,

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.data
package me.rhunk.snapenhance.core.download.data
enum class SplitMediaAssetType {
ORIGINAL, OVERLAY

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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")
}
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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)
}
}
})

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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())
}
}*/
}
}

View File

@ -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(", ")})")
}

View File

@ -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) {

View File

@ -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())

View File

@ -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())

View File

@ -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