feat: more native bridge methods (#12)
This commit is contained in:
commit
0c662d8b66
22 changed files with 486 additions and 201 deletions
1
.idea/.name
generated
1
.idea/.name
generated
|
|
@ -1 +0,0 @@
|
|||
RevengeXposed
|
||||
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
115
.idea/codeStyles/Project.xml
generated
115
.idea/codeStyles/Project.xml
generated
|
|
@ -1,121 +1,10 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="AUTODETECT_INDENTS" value="false" />
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
|
|
|
|||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/deploymentTargetDropDown.xml
generated
10
.idea/deploymentTargetDropDown.xml
generated
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/deploymentTargetSelector.xml
generated
11
.idea/deploymentTargetSelector.xml
generated
|
|
@ -4,14 +4,9 @@
|
|||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-10-11T15:31:32.125634Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=4H4HW8U8Z98LCY89" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="RevengeXposed.app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
|
|
|||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
|
@ -13,7 +13,6 @@
|
|||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
|||
30
.idea/jarRepositories.xml
generated
30
.idea/jarRepositories.xml
generated
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/kotlinc.xml
generated
6
.idea/kotlinc.xml
generated
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.1.10" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/migrations.xml
generated
10
.idea/migrations.xml
generated
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
9
.idea/misc.xml
generated
|
|
@ -1,10 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -6,14 +6,14 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.github.revenge.xposed"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.revenge.xposed"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1202
|
||||
versionName = "1.2.2"
|
||||
targetSdk = 36
|
||||
versionCode = 1400
|
||||
versionName = "1.4.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -41,9 +41,9 @@ android {
|
|||
dependencies {
|
||||
compileOnly("de.robv.android.xposed:api:82")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.16.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||
implementation("io.ktor:ktor-client-core:3.1.1")
|
||||
implementation("io.ktor:ktor-client-cio:3.1.1")
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
|
||||
implementation("io.ktor:ktor-client-core:3.2.3")
|
||||
implementation("io.ktor:ktor-client-cio:3.2.3")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ class Constants {
|
|||
const val CACHE_DIR = "cache/pyoncord"
|
||||
const val MAIN_SCRIPT_FILE = "bundle.js"
|
||||
|
||||
|
||||
const val LOG_TAG = "Revenge"
|
||||
|
||||
const val LOADER_NAME = "RevengeXposed"
|
||||
|
||||
const val USER_AGENT = "RevengeXposed"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import io.github.revenge.xposed.modules.*
|
|||
import io.github.revenge.xposed.modules.appearance.FontsModule
|
||||
import io.github.revenge.xposed.modules.appearance.SysColorsModule
|
||||
import io.github.revenge.xposed.modules.appearance.ThemeModule
|
||||
import io.github.revenge.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
||||
import io.github.revenge.xposed.modules.bridge.BridgeModule
|
||||
|
||||
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
|
|
@ -18,12 +19,15 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
|||
private val modules = mutableListOf(
|
||||
HookScriptLoaderModule(),
|
||||
BridgeModule(),
|
||||
AdditionalBridgeMethodsModule(),
|
||||
PluginsModule(),
|
||||
UpdaterModule(),
|
||||
FixResourcesModule(),
|
||||
LogBoxModule(),
|
||||
CacheModule(),
|
||||
FontsModule(),
|
||||
ThemeModule(),
|
||||
SysColorsModule(),
|
||||
SysColorsModule()
|
||||
)
|
||||
|
||||
init {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
package io.github.revenge.xposed.modules
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import io.github.revenge.xposed.Module
|
||||
import io.github.revenge.xposed.Utils.Log
|
||||
import io.github.revenge.xposed.modules.bridge.BridgeModule
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
* A module for caching versioned modules finds and blacklist, and assets mappings.
|
||||
*
|
||||
* See [ModulesCache] and [AssetsCache] for the cache structure.
|
||||
*
|
||||
* ## Methods
|
||||
*
|
||||
* ### Modules cache
|
||||
*
|
||||
* - `revenge.caches.modules.read() : { blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }, version: number } | null`
|
||||
* - Reads the modules cache. Returns `null` if the cache file does not exist or is invalid.
|
||||
*
|
||||
* - `revenge.caches.modules.write(blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }) : void`
|
||||
* - Writes the modules cache.
|
||||
*
|
||||
* ### Assets cache
|
||||
*
|
||||
* - `revenge.caches.assets.read() : { data: { [name: string]: { [type: string]: number } }, version: number } | null`
|
||||
* - Reads the assets cache. Returns `null` if the cache file does not exist or is invalid.
|
||||
*
|
||||
* - `revenge.caches.assets.write(data: { [name: string]: { [type: string]: number } }) : void`
|
||||
* - Writes the assets cache.
|
||||
*
|
||||
* The caches are versioned and tied to the app version code. When the app is updated,
|
||||
* the caches are invalidated and new cache files are created.
|
||||
*/
|
||||
class CacheModule : Module() {
|
||||
private companion object {
|
||||
const val CACHE_DIR = "revenge"
|
||||
|
||||
const val MODULES_CACHE_PREFIX = "modules"
|
||||
const val ASSETS_CACHE_PREFIX = "assets"
|
||||
}
|
||||
|
||||
private lateinit var modulesCache: ModulesCache
|
||||
private lateinit var assetsCache: AssetsCache
|
||||
|
||||
override fun onContext(context: Context) = with(context) {
|
||||
val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
|
||||
val (_, _, _, versionCode) = getAppInfo()
|
||||
|
||||
val modulesCacheFile = File(
|
||||
revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
|
||||
).apply { asFile() }
|
||||
|
||||
val assetsCacheFile = File(
|
||||
revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
|
||||
).apply { asFile() }
|
||||
|
||||
BridgeModule.registerMethod("revenge.caches.modules.read") {
|
||||
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
|
||||
?.let {
|
||||
modulesCache = it
|
||||
it.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
BridgeModule.registerMethod("revenge.caches.modules.write") {
|
||||
val (blacklist, finds) = it
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
modulesCache = ModulesCache(
|
||||
blacklist as ArrayList<Double>, finds as HashMap<String, HashMap<String, Double>?>
|
||||
).apply { saveToFile(modulesCacheFile) }
|
||||
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
|
||||
}
|
||||
|
||||
BridgeModule.registerMethod("revenge.caches.assets.read") {
|
||||
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
|
||||
?.let {
|
||||
assetsCache = it
|
||||
it.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
BridgeModule.registerMethod("revenge.caches.assets.write") {
|
||||
val (data) = it
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
assetsCache =
|
||||
AssetsCache(data as HashMap<String, HashMap<String, Double>>).apply { saveToFile(assetsCacheFile) }
|
||||
Log.i("Assets cache saved: ${assetsCacheFile.absolutePath} (count: ${data.size})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheVersionMismatchException(expected: Int, actual: Int) :
|
||||
Throwable("Expected cache version: $expected, but got version: $actual")
|
||||
|
||||
data class ModulesCache(
|
||||
val blacklist: List<Double>, val finds: Map<String, Map<String, Double>?>
|
||||
) {
|
||||
fun saveToFile(file: File) {
|
||||
val atomic = AtomicFile(file)
|
||||
var fos: FileOutputStream? = null
|
||||
try {
|
||||
fos = atomic.startWrite()
|
||||
val out = DataOutputStream(BufferedOutputStream(fos))
|
||||
|
||||
out.writeInt(VERSION)
|
||||
|
||||
// Write blacklist
|
||||
out.writeInt(blacklist.size)
|
||||
for (num in blacklist) out.writeInt(num.toInt())
|
||||
|
||||
// Write finds
|
||||
out.writeInt(finds.size)
|
||||
for ((filter, matches) in finds) {
|
||||
out.writeUTF(filter)
|
||||
if (matches == null) {
|
||||
out.writeBoolean(false) // null marker
|
||||
} else {
|
||||
out.writeBoolean(true) // not null
|
||||
out.writeInt(matches.size)
|
||||
for ((id, flag) in matches) {
|
||||
out.writeInt(id.toInt())
|
||||
out.writeInt(flag.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.flush()
|
||||
atomic.finishWrite(fos)
|
||||
} catch (t: Throwable) {
|
||||
if (fos != null) atomic.failWrite(fos)
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
fun toMap(): Map<String, Any> {
|
||||
val findsMap = finds.mapValues { (_, matches) -> matches?.map { (k, v) -> k to v }?.toMap() }
|
||||
|
||||
return mapOf("blacklist" to blacklist, "finds" to findsMap, "version" to VERSION)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VERSION = 3
|
||||
|
||||
fun loadFromFileOrNull(file: File): ModulesCache? {
|
||||
if (!file.exists() || file.length() <= 0L) return null
|
||||
val atomic = AtomicFile(file)
|
||||
try {
|
||||
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
|
||||
val version = input.readInt()
|
||||
if (version != VERSION) throw CacheVersionMismatchException(VERSION, version)
|
||||
|
||||
val blacklistSize = input.readInt()
|
||||
val blacklist = MutableList(blacklistSize) { input.readInt().toDouble() }
|
||||
|
||||
val findsSize = input.readInt()
|
||||
val finds = mutableMapOf<String, Map<String, Double>?>()
|
||||
repeat(findsSize) {
|
||||
val filter = input.readUTF()
|
||||
val hasMatches = input.readBoolean()
|
||||
if (!hasMatches) {
|
||||
finds[filter] = null
|
||||
} else {
|
||||
val mapSize = input.readInt()
|
||||
val matches = mutableMapOf<String, Double>()
|
||||
repeat(mapSize) {
|
||||
val id = input.readInt().toString()
|
||||
val flag = input.readInt().toDouble()
|
||||
matches[id] = flag
|
||||
}
|
||||
finds[filter] = matches
|
||||
}
|
||||
}
|
||||
|
||||
return ModulesCache(blacklist, finds)
|
||||
}
|
||||
} catch (e: CacheVersionMismatchException) {
|
||||
Log.i("Modules cache version mismatch: ${e.message}")
|
||||
} catch (e: EOFException) {
|
||||
Log.e("Modules cache corrupt: ${e.message}")
|
||||
} catch (e: IOException) {
|
||||
Log.e("Failed to read modules cache: ${e.message}")
|
||||
}
|
||||
runCatching { file.delete() }
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AssetsCache(
|
||||
val data: Map<String, Map<String, Double>>
|
||||
) {
|
||||
fun saveToFile(file: File) {
|
||||
val atomic = AtomicFile(file)
|
||||
var fos: FileOutputStream? = null
|
||||
try {
|
||||
fos = atomic.startWrite()
|
||||
val out = DataOutputStream(BufferedOutputStream(fos))
|
||||
|
||||
out.writeInt(VERSION)
|
||||
|
||||
out.writeInt(data.size)
|
||||
for ((name, mappings) in data) {
|
||||
out.writeUTF(name)
|
||||
out.writeInt(mappings.size)
|
||||
for ((type, id) in mappings) {
|
||||
out.writeUTF(type)
|
||||
out.writeInt(id.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
out.flush()
|
||||
atomic.finishWrite(fos)
|
||||
} catch (t: Throwable) {
|
||||
if (fos != null) atomic.failWrite(fos)
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
fun toMap(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"data" to data.mapValues { (_, mappings) ->
|
||||
mappings.map { (type, id) -> type to id }.toMap()
|
||||
}, "version" to VERSION
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VERSION = 2
|
||||
|
||||
fun loadFromFileOrNull(file: File): AssetsCache? {
|
||||
if (!file.exists() || file.length() <= 0L) return null
|
||||
val atomic = AtomicFile(file)
|
||||
try {
|
||||
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
|
||||
val version = input.readInt()
|
||||
if (version != VERSION) throw CacheVersionMismatchException(VERSION, version)
|
||||
|
||||
val dataSize = input.readInt()
|
||||
val data = mutableMapOf<String, Map<String, Double>>()
|
||||
|
||||
repeat(dataSize) {
|
||||
val name = input.readUTF()
|
||||
val mappingsSize = input.readInt()
|
||||
val mappings = mutableMapOf<String, Double>()
|
||||
|
||||
repeat(mappingsSize) {
|
||||
val type = input.readUTF()
|
||||
val id = input.readInt().toDouble()
|
||||
mappings[type] = id
|
||||
}
|
||||
|
||||
data[name] = mappings
|
||||
}
|
||||
|
||||
return AssetsCache(data)
|
||||
}
|
||||
} catch (e: CacheVersionMismatchException) {
|
||||
Log.i("Assets cache version mismatch: ${e.message}")
|
||||
} catch (e: EOFException) {
|
||||
Log.e("Assets cache corrupt: ${e.message}")
|
||||
} catch (e: IOException) {
|
||||
Log.e("Failed to read assets cache: ${e.message}")
|
||||
}
|
||||
runCatching { file.delete() }
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package io.github.revenge.xposed.modules
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import io.github.revenge.xposed.Module
|
||||
import io.github.revenge.xposed.Utils.Log
|
||||
import io.github.revenge.xposed.modules.bridge.BridgeModule
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
* Manages the states of plugins, allowing reading and writing of plugin states to a file.
|
||||
*
|
||||
* ## Methods
|
||||
*
|
||||
* - `revenge.plugins.states.read(): { flags: { [pluginId: string]: number } }`
|
||||
* - Reads the current plugin states from the file and returns them as a map.
|
||||
*
|
||||
* - `revenge.plugins.states.write(flags: { [pluginId: string]: number }): void`
|
||||
* - Writes the provided plugin states to the file.
|
||||
*/
|
||||
class PluginsModule : Module() {
|
||||
private companion object {
|
||||
const val DATA_DIR = "revenge/plugins"
|
||||
const val STATES_FILE = "states"
|
||||
}
|
||||
|
||||
private lateinit var states: PluginStates
|
||||
|
||||
override fun onContext(context: Context) = with(context) {
|
||||
val dataDir = File(filesDir, DATA_DIR)
|
||||
|
||||
val statesFile = File(
|
||||
dataDir, STATES_FILE
|
||||
).apply { asFile() }
|
||||
|
||||
BridgeModule.registerMethod("revenge.plugins.states.read") {
|
||||
if (::states.isInitialized) states.toMap() else
|
||||
PluginStates.loadFromFileOrNull(statesFile)?.let {
|
||||
states = it
|
||||
it.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
BridgeModule.registerMethod("revenge.plugins.states.write") {
|
||||
val (flags) = it
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
states = PluginStates(
|
||||
flags as Map<String, Double>
|
||||
).apply { saveToFile(statesFile) }
|
||||
Log.i("Plugin states saved: ${statesFile.absolutePath}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class UnsupportedPluginStatesVersionException(version: Int) :
|
||||
Throwable("Unsupported plugin states version: $version")
|
||||
|
||||
data class PluginStates(
|
||||
val flags: Map<String, Double>
|
||||
) {
|
||||
fun saveToFile(file: File) {
|
||||
val atomic = AtomicFile(file)
|
||||
var fos: FileOutputStream? = null
|
||||
try {
|
||||
fos = atomic.startWrite()
|
||||
val out = DataOutputStream(BufferedOutputStream(fos))
|
||||
|
||||
out.writeInt(CURRENT_VERSION)
|
||||
|
||||
// Write flags
|
||||
out.writeInt(flags.size)
|
||||
for ((id, flags) in flags) {
|
||||
out.writeUTF(id)
|
||||
out.writeInt(flags.toInt())
|
||||
}
|
||||
|
||||
out.flush()
|
||||
atomic.finishWrite(fos)
|
||||
} catch (t: Throwable) {
|
||||
if (fos != null) atomic.failWrite(fos)
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
fun toMap(): Map<String, Any> = mapOf("flags" to flags)
|
||||
|
||||
companion object {
|
||||
const val CURRENT_VERSION = 1
|
||||
|
||||
fun loadFromFileOrNull(file: File): PluginStates? {
|
||||
if (!file.exists() || file.length() <= 0L) return null
|
||||
val atomic = AtomicFile(file)
|
||||
|
||||
try {
|
||||
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
|
||||
when (val version = input.readInt()) {
|
||||
1 -> return loadV1FromFileOrNull(input)
|
||||
else -> throw UnsupportedPluginStatesVersionException(version)
|
||||
}
|
||||
}
|
||||
} catch (e: UnsupportedPluginStatesVersionException) {
|
||||
Log.i(e.message!!)
|
||||
} catch (e: EOFException) {
|
||||
Log.e("Plugin states corrupt: ${e.message}")
|
||||
} catch (e: IOException) {
|
||||
Log.e("Failed to read plugin states: ${e.message}")
|
||||
}
|
||||
|
||||
runCatching { file.delete() }
|
||||
return null
|
||||
}
|
||||
|
||||
private fun loadV1FromFileOrNull(input: DataInputStream): PluginStates {
|
||||
val flagsSize = input.readInt()
|
||||
val flags = mutableMapOf<String, Double>()
|
||||
|
||||
repeat(flagsSize) {
|
||||
flags[input.readUTF()] = input.readInt().toDouble()
|
||||
}
|
||||
|
||||
return PluginStates(flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,9 +47,7 @@ class UpdaterModule : Module() {
|
|||
private lateinit var etag: File
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
var job: Job? = null
|
||||
private set
|
||||
|
||||
private const val TIMEOUT_CACHED = 5000L
|
||||
private const val TIMEOUT = 10000L
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package io.github.revenge.xposed.modules.bridge
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import io.github.revenge.xposed.Module
|
||||
|
||||
class AdditionalBridgeMethodsModule : Module() {
|
||||
override fun onContext(context: Context) = with(context) {
|
||||
BridgeModule.registerMethod("revenge.alertError") {
|
||||
val (error, version) = it
|
||||
val app = getAppInfo()
|
||||
val errorString = "$error"
|
||||
|
||||
val clipboard = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Stack Trace", errorString)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Revenge Error")
|
||||
.setMessage(
|
||||
"""
|
||||
Revenge: $version
|
||||
${app.name}: ${app.version} (${app.versionCode})
|
||||
Device: ${Build.MANUFACTURER} ${Build.MODEL}
|
||||
|
||||
|
||||
""".trimIndent() + errorString
|
||||
)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton("Copy") { dialog, _ ->
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(applicationContext, "Copied stack trace", Toast.LENGTH_SHORT).show()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.9.2")
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue