diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 91a38563..3bfb6c14 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -85,6 +85,10 @@ dependencies { implementation(libs.androidx.activity.ktx) implementation(libs.androidx.material3) implementation(libs.androidx.material) + + debugImplementation("androidx.compose.ui:ui-tooling:1.4.3") + implementation("androidx.compose.ui:ui-tooling-preview:1.4.3") + implementation(kotlin("reflect")) } afterEvaluate { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bb1de7db..48d16183 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,16 +7,16 @@ - - - + + + + - NavHost(navController, startDestination = "main", Modifier.padding(innerPadding)) { - navigation(MainSections.HOME.route, "main") { - MainSections.values().toList().forEach { section -> - composable(section.route) { - section.content() - } - } - } - } + navigation.NavigationHost(navController = navController, innerPadding = innerPadding) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt index 18d7cce0..c93b4117 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt @@ -1,113 +1,100 @@ package me.rhunk.snapenhance.manager import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Group -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Stars import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState -import me.rhunk.snapenhance.manager.sections.NotImplemented +import me.rhunk.snapenhance.manager.data.ManagerContext -enum class MainSections( - val route: String, - val title: String, - val icon: ImageVector, - val content: @Composable () -> Unit +class Navigation( + private val context: ManagerContext ) { - DOWNLOADS( - route = "downloads", - title = "Downloads", - icon = Icons.Filled.Download, - content = { NotImplemented() } - ), - FEATURES( - route = "features", - title = "Features", - icon = Icons.Filled.Stars, - content = { NotImplemented() } - ), - HOME( - route = "home", - title = "Home", - icon = Icons.Filled.Home, - content = { NotImplemented() } - ), - FRIENDS( - route = "friends", - title = "Friends", - icon = Icons.Filled.Group, - content = { NotImplemented() } - ), - DEBUG( - route = "debug", - title = "Debug", - icon = Icons.Filled.BugReport, - content = { NotImplemented() } - ); -} + @Composable + fun NavigationHost( + navController: NavHostController, + innerPadding: PaddingValues + ) { + val sections = remember { EnumSection.values().toList().map { + it to it.section.constructors.first().call() + }.onEach { (_, instance) -> + instance.manager = context + instance.navController = navController + } } + val homeSection = EnumSection.HOME -@Composable -fun NavBar( - navController: NavController -) { - NavigationBar { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - MainSections.values().toList().forEach { section -> - fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true - - NavigationBarItem( - modifier = Modifier - .requiredWidth(120.dp) - .fillMaxHeight(), - icon = { - val iconOffset by animateDpAsState( - if (selected()) 0.dp else 10.dp, - label = "" - ) - Icon( - imageVector = section.icon, - contentDescription = null, - modifier = Modifier.offset(y = iconOffset) - ) - }, - - label = { - val labelOffset by animateDpAsState( - if (selected()) 0.dp else (-5).dp, - label = "" - ) - Text(text = if (selected()) section.title else "", modifier = Modifier.offset(y = labelOffset)) - }, - selected = selected(), - onClick = { - navController.navigate(section.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + NavHost(navController, startDestination = homeSection.route, Modifier.padding(innerPadding)) { + sections.forEach { (section, instance) -> + composable(section.route) { + instance.Content() } - ) + } } } -} \ No newline at end of file + + @Composable + fun NavBar( + navController: NavController + ) { + NavigationBar { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + EnumSection.values().toList().forEach { section -> + fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true + + NavigationBarItem( + modifier = Modifier + .requiredWidth(120.dp) + .fillMaxHeight(), + icon = { + val iconOffset by animateDpAsState( + if (selected()) 0.dp else 10.dp, + label = "" + ) + Icon( + imageVector = section.icon, + contentDescription = null, + modifier = Modifier.offset(y = iconOffset) + ) + }, + + label = { + val labelOffset by animateDpAsState( + if (selected()) 0.dp else (-5).dp, + label = "" + ) + Text(text = if (selected()) section.title else "", modifier = Modifier.offset(y = labelOffset)) + }, + selected = selected(), + onClick = { + navController.navigate(section.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt new file mode 100644 index 00000000..17162e24 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt @@ -0,0 +1,61 @@ +package me.rhunk.snapenhance.manager + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Stars +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation.NavController +import me.rhunk.snapenhance.manager.data.ManagerContext +import me.rhunk.snapenhance.manager.sections.FeaturesSection +import me.rhunk.snapenhance.manager.sections.HomeSection +import me.rhunk.snapenhance.manager.sections.NotImplemented +import kotlin.reflect.KClass + +enum class EnumSection( + val route: String, + val title: String, + val icon: ImageVector, + val section: KClass = NotImplemented::class +) { + DOWNLOADS( + route = "downloads", + title = "Downloads", + icon = Icons.Filled.Download + ), + FEATURES( + route = "features", + title = "Features", + icon = Icons.Filled.Stars, + section = FeaturesSection::class + ), + HOME( + route = "home", + title = "Home", + icon = Icons.Filled.Home, + section = HomeSection::class + ), + FRIENDS( + route = "friends", + title = "Friends", + icon = Icons.Filled.Group + ), + DEBUG( + route = "debug", + title = "Debug", + icon = Icons.Filled.BugReport + ); +} + + + +open class Section { + lateinit var manager: ManagerContext + lateinit var navController: NavController + + @Composable + open fun Content() { NotImplemented() } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt index 70df5a95..eedc6b14 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt @@ -74,8 +74,6 @@ val md_theme_dark_scrim = Color(0xFF000000) val seed = Color(0xFF6750A4) - - private val LightThemeColors = lightColorScheme( primary = md_theme_light_primary, onPrimary = md_theme_light_onPrimary, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/InstallationSummary.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/InstallationSummary.kt new file mode 100644 index 00000000..f7b0150d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/InstallationSummary.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.manager.data + + +data class SnapchatAppInfo( + val version: String, + val versionCode: Long +) + +data class ModMappingsInfo( + val generatedSnapchatVersion: Long, + val isOutdated: Boolean +) + +data class InstallationSummary( + val snapchatInfo: SnapchatAppInfo?, + val mappingsInfo: ModMappingsInfo? +) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt new file mode 100644 index 00000000..487a7907 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt @@ -0,0 +1,35 @@ +package me.rhunk.snapenhance.manager.data + +import android.content.Context +import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper +import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper +import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper + +class ManagerContext( + private val context: Context +) { + private val config = ConfigWrapper() + private val translation = TranslationWrapper() + private val mappings = MappingsWrapper(context) + + init { + config.loadFromContext(context) + translation.loadFromContext(context) + mappings.apply { loadFromContext(context) }.init() + } + + fun getInstallationSummary() = InstallationSummary( + snapchatInfo = mappings.getSnapchatPackageInfo()?.let { + SnapchatAppInfo( + version = it.versionName, + versionCode = it.longVersionCode + ) + }, + mappingsInfo = if (mappings.isMappingsLoaded()) { + ModMappingsInfo( + generatedSnapchatVersion = mappings.getGeneratedBuildNumber(), + isOutdated = mappings.isMappingsOutdated() + ) + } else null + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/DebugSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/DebugSection.kt new file mode 100644 index 00000000..b9b0f606 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/DebugSection.kt @@ -0,0 +1,4 @@ +package me.rhunk.snapenhance.manager.sections + +class DebugSection { +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/FeaturesSection.kt new file mode 100644 index 00000000..e1968dcd --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/FeaturesSection.kt @@ -0,0 +1,52 @@ +package me.rhunk.snapenhance.manager.sections + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.lazy.LazyColumn +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.manager.Section + +class FeaturesSection : Section() { + @Composable + @Preview + override fun Content() { + Column { + Text( + text = "Features", + modifier = Modifier.padding(all = 15.dp), + fontSize = 20.sp + ) + LazyColumn( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + items(100) { index -> + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .height(70.dp) + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + Text(text = "Feature $index", modifier = Modifier.padding(all = 15.dp)) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/HomeSection.kt new file mode 100644 index 00000000..5e89be9b --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/HomeSection.kt @@ -0,0 +1,108 @@ +package me.rhunk.snapenhance.manager.sections + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +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.Map +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.manager.Section +import me.rhunk.snapenhance.manager.data.InstallationSummary + +class HomeSection : Section() { + companion object { + val cardMargin = 10.dp + } + + @OptIn(ExperimentalLayoutApi::class) + @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() + ) { + FlowRow( + 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 = {}, modifier = Modifier.height(40.dp)) { + Icon(Icons.Filled.Refresh, contentDescription = "Refresh") + } + } + } + } + + @Composable + @Preview + override fun Content() { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(ScrollState(0)) + ) { + Text( + "SnapEnhance", + fontSize = 32.sp, + modifier = Modifier.padding(32.dp) + ) + + 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(manager.getInstallationSummary()) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt index 272ef529..d5f2f65e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt @@ -1,18 +1,20 @@ package me.rhunk.snapenhance.manager.sections +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import me.rhunk.snapenhance.manager.Section -@Composable -fun NotImplemented() { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = "Not Implemented") +class NotImplemented : Section() { + @Composable + override fun Content() { + Column { + Text(text = "Not implemented yet!") + } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/launcher_icon_monochrome.xml b/app/src/main/res/drawable/launcher_icon_monochrome.xml new file mode 100644 index 00000000..d98ea0b7 --- /dev/null +++ b/app/src/main/res/drawable/launcher_icon_monochrome.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/core/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml similarity index 78% rename from core/src/main/res/mipmap-anydpi-v26/launcher_icon.xml rename to app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml index ae6391b6..0a1cdd2d 100644 --- a/core/src/main/res/mipmap-anydpi-v26/launcher_icon.xml +++ b/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/core/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml b/app/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml similarity index 100% rename from core/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml rename to app/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml diff --git a/core/src/main/res/mipmap-hdpi/launcher_icon_foreground.png b/app/src/main/res/mipmap-hdpi/launcher_icon_foreground.png similarity index 100% rename from core/src/main/res/mipmap-hdpi/launcher_icon_foreground.png rename to app/src/main/res/mipmap-hdpi/launcher_icon_foreground.png diff --git a/core/src/main/res/mipmap-hdpi/launcher_icon_round.png b/app/src/main/res/mipmap-hdpi/launcher_icon_round.png similarity index 100% rename from core/src/main/res/mipmap-hdpi/launcher_icon_round.png rename to app/src/main/res/mipmap-hdpi/launcher_icon_round.png diff --git a/core/src/main/res/mipmap-mdpi/launcher_icon_foreground.png b/app/src/main/res/mipmap-mdpi/launcher_icon_foreground.png similarity index 100% rename from core/src/main/res/mipmap-mdpi/launcher_icon_foreground.png rename to app/src/main/res/mipmap-mdpi/launcher_icon_foreground.png diff --git a/core/src/main/res/mipmap-mdpi/launcher_icon_round.png b/app/src/main/res/mipmap-mdpi/launcher_icon_round.png similarity index 100% rename from core/src/main/res/mipmap-mdpi/launcher_icon_round.png rename to app/src/main/res/mipmap-mdpi/launcher_icon_round.png diff --git a/core/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png b/app/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png similarity index 100% rename from core/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png rename to app/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png diff --git a/core/src/main/res/mipmap-xhdpi/launcher_icon_round.png b/app/src/main/res/mipmap-xhdpi/launcher_icon_round.png similarity index 100% rename from core/src/main/res/mipmap-xhdpi/launcher_icon_round.png rename to app/src/main/res/mipmap-xhdpi/launcher_icon_round.png diff --git a/core/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png b/app/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png similarity index 100% rename from core/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png rename to app/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png diff --git a/core/src/main/res/mipmap-xxhdpi/launcher_icon_round.png b/app/src/main/res/mipmap-xxhdpi/launcher_icon_round.png similarity index 100% rename from core/src/main/res/mipmap-xxhdpi/launcher_icon_round.png rename to app/src/main/res/mipmap-xxhdpi/launcher_icon_round.png diff --git a/core/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png b/app/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png similarity index 100% rename from core/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png rename to app/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png diff --git a/core/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png b/app/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png similarity index 100% rename from core/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png rename to app/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt index 3dc95eba..6c9dd3f0 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -91,7 +91,6 @@ class BridgeClient( fun deleteFile(fileType: BridgeFileType) = service.deleteFile(fileType.value) - fun isFileExists(fileType: BridgeFileType) = service.isFileExists(fileType.value) fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/FileLoaderWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/FileLoaderWrapper.kt new file mode 100644 index 00000000..a98e597a --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/FileLoaderWrapper.kt @@ -0,0 +1,35 @@ +package me.rhunk.snapenhance.bridge + +import android.content.Context +import me.rhunk.snapenhance.bridge.types.BridgeFileType + +open class FileLoaderWrapper( + private val fileType: BridgeFileType, + private val defaultContent: ByteArray +) { + lateinit var isFileExists: () -> Boolean + lateinit var write: (ByteArray) -> Unit + lateinit var read: () -> ByteArray + lateinit var delete: () -> Unit + + fun loadFromContext(context: Context) { + val file = fileType.resolve(context) + isFileExists = { file.exists() } + read = { + if (!file.exists()) { + file.createNewFile() + file.writeBytes("{}".toByteArray(Charsets.UTF_8)) + } + file.readBytes() + } + write = { file.writeBytes(it) } + delete = { file.delete() } + } + + fun loadFromBridge(bridgeClient: BridgeClient) { + isFileExists = { bridgeClient.isFileExists(fileType) } + read = { bridgeClient.createAndReadFile(fileType, defaultContent) } + write = { bridgeClient.writeFile(fileType, it) } + delete = { bridgeClient.deleteFile(fileType) } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt index df338ac4..4b9fc6c0 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt @@ -5,6 +5,7 @@ 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.config.ConfigAccessor import me.rhunk.snapenhance.config.ConfigProperty @@ -14,16 +15,14 @@ class ConfigWrapper: ConfigAccessor() { private val gson = GsonBuilder().setPrettyPrinting().create() } - private lateinit var isFileExistsAction: () -> Boolean - private lateinit var writeFileAction: (ByteArray) -> Unit - private lateinit var readFileAction: () -> ByteArray + private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) fun load() { ConfigProperty.sortedByCategory().forEach { key -> set(key, key.valueContainer) } - if (!isFileExistsAction()) { + if (!file.isFileExists()) { writeConfig() return } @@ -37,7 +36,7 @@ class ConfigWrapper: ConfigAccessor() { } private fun loadConfig() { - val configContent = readFileAction() + val configContent = file.read() val configObject: JsonObject = gson.fromJson( configContent.toString(Charsets.UTF_8), @@ -54,27 +53,17 @@ class ConfigWrapper: ConfigAccessor() { entries().forEach { (key, value) -> configObject.addProperty(key.name, value.read()) } - writeFileAction(gson.toJson(configObject).toByteArray(Charsets.UTF_8)) + + file.write(gson.toJson(configObject).toByteArray(Charsets.UTF_8)) } fun loadFromContext(context: Context) { - val configFile = BridgeFileType.CONFIG.resolve(context) - isFileExistsAction = { configFile.exists() } - readFileAction = { - if (!configFile.exists()) { - configFile.createNewFile() - configFile.writeBytes("{}".toByteArray(Charsets.UTF_8)) - } - configFile.readBytes() - } - writeFileAction = { configFile.writeBytes(it) } + file.loadFromContext(context) load() } fun loadFromBridge(bridgeClient: BridgeClient) { - isFileExistsAction = { bridgeClient.isFileExists(BridgeFileType.CONFIG) } - readFileAction = { bridgeClient.createAndReadFile(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) } - writeFileAction = { bridgeClient.writeFile(BridgeFileType.CONFIG, it) } + file.loadFromBridge(bridgeClient) load() } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt new file mode 100644 index 00000000..ecf08d1f --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt @@ -0,0 +1,157 @@ +package me.rhunk.snapenhance.bridge.wrapper + +import android.content.Context +import com.google.gson.GsonBuilder +import com.google.gson.JsonElement +import com.google.gson.JsonParser +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.bridge.FileLoaderWrapper +import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapmapper.Mapper +import me.rhunk.snapmapper.impl.BCryptClassMapper +import me.rhunk.snapmapper.impl.CallbackMapper +import me.rhunk.snapmapper.impl.DefaultMediaItemMapper +import me.rhunk.snapmapper.impl.EnumMapper +import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper +import me.rhunk.snapmapper.impl.MediaQualityLevelProviderMapper +import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper +import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper +import me.rhunk.snapmapper.impl.PlusSubscriptionMapper +import me.rhunk.snapmapper.impl.ScCameraSettingsMapper +import me.rhunk.snapmapper.impl.StoryBoostStateMapper +import java.util.concurrent.ConcurrentHashMap +import kotlin.system.measureTimeMillis + +class MappingsWrapper( + private val context: Context, +) : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) { + companion object { + private val gson = GsonBuilder().setPrettyPrinting().create() + private val mappers = arrayOf( + BCryptClassMapper::class, + CallbackMapper::class, + DefaultMediaItemMapper::class, + MediaQualityLevelProviderMapper::class, + EnumMapper::class, + OperaPageViewControllerMapper::class, + PlatformAnalyticsCreatorMapper::class, + PlusSubscriptionMapper::class, + ScCameraSettingsMapper::class, + StoryBoostStateMapper::class, + FriendsFeedEventDispatcherMapper::class + ) + } + + private val mappings = ConcurrentHashMap() + private var snapBuildNumber: Long = 0 + + @Suppress("deprecation") + fun init() { + snapBuildNumber = getSnapchatVersionCode() + + if (isFileExists()) { + runCatching { + loadCached() + }.onFailure { + delete() + } + } + } + + fun getSnapchatPackageInfo() = runCatching { + context.packageManager.getPackageInfo( + Constants.SNAPCHAT_PACKAGE_NAME, + 0 + ) + }.getOrNull() + + fun getSnapchatVersionCode() = getSnapchatPackageInfo()?.longVersionCode ?: -1 + fun getApplicationSourceDir() = getSnapchatPackageInfo()?.applicationInfo?.sourceDir + fun getGeneratedBuildNumber() = snapBuildNumber + + fun isMappingsOutdated(): Boolean { + return snapBuildNumber != getSnapchatVersionCode() || isMappingsLoaded().not() + } + + fun isMappingsLoaded(): Boolean { + return mappings.isNotEmpty() + } + + private fun loadCached() { + if (!isFileExists()) { + throw Exception("Mappings file does not exist") + } + val mappingsObject = JsonParser.parseString(read().toString(Charsets.UTF_8)).asJsonObject.also { + snapBuildNumber = it["snap_build_number"].asLong + } + + mappingsObject.entrySet().forEach { (key, value): Map.Entry -> + if (value.isJsonArray) { + mappings[key] = gson.fromJson(value, ArrayList::class.java) + return@forEach + } + if (value.isJsonObject) { + mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java) + return@forEach + } + mappings[key] = value.asString + } + } + + fun refresh() { + val mapper = Mapper(*mappers) + + runCatching { + mapper.loadApk(getApplicationSourceDir() ?: throw Exception("Failed to get APK")) + }.onFailure { + throw Exception("Failed to load APK", it) + } + + measureTimeMillis { + val result = mapper.start().apply { + addProperty("snap_build_number", snapBuildNumber) + } + write(result.toString().toByteArray()) + }.also { + Logger.xposedLog("Generated mappings in $it ms") + } + } + + fun getMappedObject(key: String): Any { + if (mappings.containsKey(key)) { + return mappings[key]!! + } + throw Exception("No mapping found for $key") + } + + fun getMappedObjectNullable(key: String): Any? { + return mappings[key] + } + + fun getMappedClass(className: String): Class<*> { + return context.classLoader.loadClass(getMappedObject(className) as String) + } + + fun getMappedClass(key: String, subKey: String): Class<*> { + return context.classLoader.loadClass(getMappedValue(key, subKey)) + } + + fun getMappedValue(key: String): String { + return getMappedObject(key) as String + } + + @Suppress("UNCHECKED_CAST") + fun getMappedList(key: String): List { + return listOf(getMappedObject(key) as List).flatten() + } + + fun getMappedValue(key: String, subKey: String): String { + return getMappedMap(key)[subKey] as String + } + + @Suppress("UNCHECKED_CAST") + fun getMappedMap(key: String): Map { + return getMappedObject(key) as Map + } +} \ No newline at end of file