ui: home screen

- monochrome launcher icon
This commit is contained in:
rhunk
2023-07-28 22:20:41 +02:00
parent 304bd9fe94
commit e4cab94ec7
29 changed files with 607 additions and 146 deletions

View File

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

View File

@ -7,16 +7,16 @@
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<queries>
<package android:name="com.snapchat.android" />
</queries>
<application
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:label="@string/app_name"
tools:targetApi="31"
tools:targetApi="34"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/launcher_icon">
<meta-data
android:name="xposedmodule"

View File

@ -16,10 +16,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.manager.data.ManagerContext
class MainActivity : ComponentActivity() {
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@ -27,39 +29,23 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
App()
App(ManagerContext(this))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun App() {
fun App(
context: ManagerContext
) {
val navController = rememberNavController()
val navigation = Navigation(context)
AppMaterialTheme {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "SnapEnhance") },
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Settings, contentDescription = "Settings")
}
}
)
},
containerColor = MaterialTheme.colorScheme.background,
bottomBar = { NavBar(navController = navController) }
bottomBar = { navigation.NavBar(navController = navController) }
) { innerPadding ->
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)
}
}
}

View File

@ -1,77 +1,63 @@
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(
NavHost(navController, startDestination = homeSection.route, Modifier.padding(innerPadding)) {
sections.forEach { (section, instance) ->
composable(section.route) {
instance.Content()
}
}
}
}
@Composable
fun NavBar(
navController: NavController
) {
) {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
MainSections.values().toList().forEach { section ->
EnumSection.values().toList().forEach { section ->
fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true
NavigationBarItem(
@ -110,4 +96,5 @@ fun NavBar(
)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package me.rhunk.snapenhance.manager.sections
class DebugSection {
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<group
android:scaleX="0.65"
android:scaleY="0.65"
android:translateX="175"
android:translateY="175">
<path
android:fillColor="#ffffffff"
android:pathData="m397.9,491.5h-55.1c-10.1,0 -18.4,8.2 -18.4,18.4h0c0,10.1 8.2,18.4 18.4,18.4h55.1c15.3,0 27.8,13.7 27.8,30.6s-12.5,30.6 -27.8,30.6h-55.1c-10.1,0 -18.4,8.2 -18.4,18.4h0c0,10.1 8.2,18.4 18.4,18.4h55.1c33.8,0 61.2,-30.1 61.2,-67.3s-27.4,-67.3 -61.2,-67.3Z"/>
<path
android:fillColor="#ffffffff"
android:pathData="m814.2,491.5h-62.6v-49.7h-0.1c0,-2 0.1,-4.1 0.1,-6.1 0,-152.1 -123.3,-275.5 -275.5,-275.5s-275.5,123.3 -275.5,275.5c0,2 0,4.1 0.1,6.1h-0.1v302.1c0,29.6 13.5,57.6 36.7,76l11.9,9.4c18.3,13.9 47,13.8 65.2,0l18.5,-14c7.1,-5.4 21,-5.4 28.1,0l18.5,14c18.3,13.9 46.9,13.9 65.2,0l18.5,-14c3.3,-2.5 8.2,-3.8 13.1,-4 4.9,0.2 9.7,1.5 13,4l18.5,14c18.3,13.8 46.8,13.8 65.1,0l18.5,-14c7.1,-5.4 20.9,-5.4 28,0l18.5,14c18.2,13.8 46.8,13.8 65.1,0l11.9,-9.4c23.2,-18.4 36.7,-46.4 36.7,-75.9v-117.8h62.6c33.8,0 61.2,-30.1 61.2,-67.3s-27.4,-67.3 -61.2,-67.3ZM714.8,441.9v310.2c0,16.4 -7.7,31.9 -20.7,41.8l-9.6,7.3c-7.1,5.4 -20.9,5.4 -28,0l-18.5,-14c-18.2,-13.8 -46.8,-13.8 -65.1,0l-18.5,14c-7.1,5.4 -20.9,5.4 -28,0l-18.5,-14c-8.8,-6.7 -20.1,-10.1 -31.4,-10.3v-0c-0.1,0 -0.1,0 -0.2,0 -0,0 -0.1,0 -0.1,0h0c-11.4,0.2 -22.6,3.7 -31.5,10.4l-18.5,14c-7.1,5.4 -21,5.4 -28.1,0l-18.5,-14c-18.3,-13.8 -46.9,-13.8 -65.2,0l-18.5,14c-7.1,5.4 -21,5.4 -28,0l-9.7,-7.4c-13.1,-9.9 -20.8,-25.4 -20.8,-41.8v-310.1h0.1c-0.1,-2 -0.1,-4.1 -0.1,-6.1 0,-131.9 106.9,-238.7 238.7,-238.7s238.7,106.9 238.7,238.7c0,2 -0,4.1 -0.1,6.1h0.1ZM814.2,589.5h-62.6v-61.2h62.6c15.3,0 27.8,13.7 27.8,30.6s-12.5,30.6 -27.8,30.6Z"
tools:ignore="VectorPath" />
<path
android:fillColor="#ffffffff"
android:pathData="m711.1,336.1c-6.6,-1.6 -10.6,-1.7 -17.4,-2.8 -33.1,-5.4 -65.4,-0.8 -97.3,8.2 -7,2 -13.9,3.4 -20.9,3.5 -7,-0.2 -13.9,-1.5 -20.9,-3.5 -31.9,-9 -64.2,-13.6 -97.3,-8.2 -6.8,1.1 -10.8,1.1 -17.4,2.8 -6,1.5 -7.5,11.2 -5.5,29.1 0.9,8.2 3.3,16.1 4.9,24.2 1.6,8.1 3.6,16.1 7.9,23.3 4.1,6.8 10.1,11.6 17.5,13.9 16.9,5.4 34.1,6.2 51.3,1.4 14.5,-4.1 26.1,-12.6 33.4,-25.9 5.4,-9.9 9.7,-20.4 14.5,-30.7 0.7,-1.6 1.3,-3.2 2.1,-4.8 2.2,-4.4 5,-6.3 9.6,-6.3 4.5,-0 7.3,1.8 9.6,6.3 0.8,1.6 1.4,3.2 2.1,4.8 4.8,10.3 9.1,20.8 14.5,30.7 7.2,13.4 18.9,21.9 33.4,25.9 17.1,4.8 34.4,4 51.3,-1.4 7.4,-2.3 13.4,-7.1 17.5,-13.9 4.3,-7.2 6.3,-15.1 7.9,-23.3 1.5,-8.1 4,-16 4.9,-24.2 2,-17.9 0.5,-27.6 -5.5,-29.1Z"/>
</group>
</vector>

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/launcher_icon_background"/>
<foreground android:drawable="@mipmap/launcher_icon_foreground"/>
<monochrome android:drawable="@drawable/launcher_icon_monochrome"/>
</adaptive-icon>

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

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

View File

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

View File

@ -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<String, Any>()
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<String, JsonElement> ->
if (value.isJsonArray) {
mappings[key] = gson.fromJson(value, ArrayList::class.java)
return@forEach
}
if (value.isJsonObject) {
mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java)
return@forEach
}
mappings[key] = value.asString
}
}
fun refresh() {
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 <T : Any> getMappedList(key: String): List<T> {
return listOf(getMappedObject(key) as List<T>).flatten()
}
fun getMappedValue(key: String, subKey: String): String {
return getMappedMap(key)[subKey] as String
}
@Suppress("UNCHECKED_CAST")
fun getMappedMap(key: String): Map<String, *> {
return getMappedObject(key) as Map<String, *>
}
}