ui: home screen
- monochrome launcher icon
@ -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)
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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, *>
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"/>
|
||||
</adaptive-icon>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB |