diff --git a/app/build.gradle b/app/build.gradle
index 256e6b1..f112b91 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -130,6 +130,6 @@ dependencies {
implementation(libs.xdl)
implementation(libs.shadowhook)
- compileOnly(libs.xposed.api)
+ compileOnly(libs.libxposed.api)
implementation(libs.kotlinx.serialization.json)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8ff0e43..c25d888 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,23 +21,6 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt
index 55891f2..5e5a6c3 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt
@@ -3,7 +3,7 @@ package io.github.chinosk.gakumas.localify
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
-import android.app.AndroidAppHelper
+import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
@@ -17,34 +17,32 @@ import android.view.MotionEvent
import android.widget.Toast
import com.bytedance.shadowhook.ShadowHook
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
-import de.robv.android.xposed.IXposedHookLoadPackage
-import de.robv.android.xposed.IXposedHookZygoteInit
-import de.robv.android.xposed.XC_MethodHook
-import de.robv.android.xposed.XposedBridge
-import de.robv.android.xposed.XposedHelpers
-import de.robv.android.xposed.callbacks.XC_LoadPackage
+import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
+import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
+import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig
+import io.github.chinosk.gakumas.localify.models.NativeInitProgress
+import io.github.chinosk.gakumas.localify.models.ProgramConfig
+import io.github.chinosk.gakumas.localify.ui.game_attach.InitProgressUI
+import io.github.libxposed.api.XposedInterface
+import io.github.libxposed.api.XposedModule
+import io.github.libxposed.api.XposedModuleInterface
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File
+import java.lang.reflect.Method
import java.util.Locale
import kotlin.system.measureTimeMillis
-import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
-import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
-import io.github.chinosk.gakumas.localify.mainUtils.json
-import io.github.chinosk.gakumas.localify.models.NativeInitProgress
-import io.github.chinosk.gakumas.localify.models.ProgramConfig
-import io.github.chinosk.gakumas.localify.ui.game_attach.InitProgressUI
val TAG = "GakumasLocalify"
-class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
- private lateinit var modulePath: String
- private var nativeLibLoadSuccess: Boolean
+class GakumasHookMain : XposedModule() {
+ private var modulePath: String = ""
+ private var nativeLibLoadSuccess: Boolean = false
private var alreadyInitialized = false
private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen"
private val nativeLibName = "MarryKotone"
@@ -55,160 +53,187 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var externalFilesChecked: Boolean = false
private var gameActivity: Activity? = null
- override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
-// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
-// XposedHelpers.findAndHookMethod(
-// "io.github.chinosk.gakumas.localify.MainActivity",
-// lpparam.classLoader,
-// "showToast",
-// String::class.java,
-// object : XC_MethodHook() {
-// override fun beforeHookedMethod(param: MethodHookParam) {
-// Log.d(TAG, "beforeHookedMethod hooked: ${param.args}")
-// }
-// }
-// )
-// }
+ override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
+ modulePath = getModuleApplicationInfo().sourceDir
- if (lpparam.packageName != targetPackageName) {
+ ShadowHook.init(
+ ConfigBuilder()
+ .setMode(ShadowHook.Mode.UNIQUE)
+ .build()
+ )
+
+ nativeLibLoadSuccess = try {
+ System.loadLibrary(nativeLibName)
+ true
+ } catch (_: UnsatisfiedLinkError) {
+ false
+ }
+ }
+
+ override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
+ if (param.packageName != targetPackageName) {
return
}
- XposedHelpers.findAndHookMethod(
- "android.app.Activity",
- lpparam.classLoader,
- "dispatchKeyEvent",
- KeyEvent::class.java,
- object : XC_MethodHook() {
- override fun beforeHookedMethod(param: MethodHookParam) {
- val keyEvent = param.args[0] as KeyEvent
- val keyCode = keyEvent.keyCode
- val action = keyEvent.action
- // Log.d(TAG, "Key event: keyCode=$keyCode, action=$action")
- keyboardEvent(keyCode, action)
- }
+ val classLoader = param.classLoader
+
+ hookMethod(
+ classLoader = classLoader,
+ className = "android.app.Activity",
+ methodName = "dispatchKeyEvent",
+ parameterTypes = arrayOf(KeyEvent::class.java),
+ before = { chain ->
+ val keyEvent = chain.getArg(0) as KeyEvent
+ keyboardEvent(keyEvent.keyCode, keyEvent.action)
}
)
- XposedHelpers.findAndHookMethod(
- "android.app.Activity",
- lpparam.classLoader,
- "dispatchGenericMotionEvent",
- MotionEvent::class.java,
- object : XC_MethodHook() {
- override fun beforeHookedMethod(param: MethodHookParam) {
- val motionEvent = param.args[0] as MotionEvent
- val action = motionEvent.action
+ hookMethod(
+ classLoader = classLoader,
+ className = "android.app.Activity",
+ methodName = "dispatchGenericMotionEvent",
+ parameterTypes = arrayOf(MotionEvent::class.java),
+ before = { chain ->
+ val motionEvent = chain.getArg(0) as MotionEvent
+ val action = motionEvent.action
- // 左摇杆的X和Y轴
- val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
- val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
+ val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
+ val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
+ val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
+ val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
+ val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
+ val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
+ val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
+ val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
- // 右摇杆的X和Y轴
- val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
- val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
-
- // 左扳机
- val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
-
- // 右扳机
- val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
-
- // 十字键
- val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
- val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
-
- // 处理摇杆和扳机事件
- joystickEvent(
- action,
- leftStickX,
- leftStickY,
- rightStickX,
- rightStickY,
- leftTrigger,
- rightTrigger,
- hatX,
- hatY
- )
- }
+ joystickEvent(
+ action,
+ leftStickX,
+ leftStickY,
+ rightStickX,
+ rightStickY,
+ leftTrigger,
+ rightTrigger,
+ hatX,
+ hatY
+ )
}
)
- val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader)
- XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() {
- override fun beforeHookedMethod(param: MethodHookParam) {
- super.beforeHookedMethod(param)
- Log.d(TAG, "onStart")
- val currActivity = param.thisObject as Activity
- gameActivity = currActivity
- if (getConfigError != null) {
- showGetConfigFailed(currActivity)
- }
- else {
- initGkmsConfig(currActivity)
- }
+ val activityClass = classLoader.loadClass("android.app.Activity")
+ hookAllMethods(activityClass, "onStart") { chain ->
+ Log.d(TAG, "onStart")
+ val currActivity = chain.thisObject as Activity
+ gameActivity = currActivity
+ if (getConfigError != null) {
+ showGetConfigFailed(currActivity)
+ } else {
+ initGkmsConfig(currActivity)
}
- })
+ chain.proceed()
+ }
- XposedBridge.hookAllMethods(appActivityClass, "onResume", object : XC_MethodHook() {
- override fun beforeHookedMethod(param: MethodHookParam) {
- Log.d(TAG, "onResume")
- val currActivity = param.thisObject as Activity
- gameActivity = currActivity
- if (getConfigError != null) {
- showGetConfigFailed(currActivity)
- }
- else {
- initGkmsConfig(currActivity)
- }
+ hookAllMethods(activityClass, "onResume") { chain ->
+ Log.d(TAG, "onResume")
+ val currActivity = chain.thisObject as Activity
+ gameActivity = currActivity
+ if (getConfigError != null) {
+ showGetConfigFailed(currActivity)
+ } else {
+ initGkmsConfig(currActivity)
}
- })
+ chain.proceed()
+ }
- val cls = lpparam.classLoader.loadClass("com.unity3d.player.UnityPlayer")
- XposedHelpers.findAndHookMethod(
- cls,
- "loadNative",
- String::class.java,
- object : XC_MethodHook() {
- @SuppressLint("UnsafeDynamicallyLoadedCode")
- override fun afterHookedMethod(param: MethodHookParam) {
- super.afterHookedMethod(param)
+ val unityPlayerClass = classLoader.loadClass("com.unity3d.player.UnityPlayer")
+ val loadNativeMethod = unityPlayerClass.getDeclaredMethod("loadNative", String::class.java)
- Log.i(TAG, "UnityPlayer.loadNative")
-
- if (alreadyInitialized) {
- return
- }
-
- val app = AndroidAppHelper.currentApplication()
- if (nativeLibLoadSuccess) {
- showToast("lib$nativeLibName.so loaded.")
- }
- else {
- showToast("Load native library lib$nativeLibName.so failed.")
- return
- }
-
- if (!gkmsDataInited) {
- requestConfig(app.applicationContext)
- }
-
- FilesChecker.initDir(app.filesDir, modulePath)
- initHook(
- "${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
- File(
- app.filesDir.absolutePath,
- FilesChecker.localizationFilesDir
- ).absolutePath
- )
-
- alreadyInitialized = true
- }
- })
+ hook(loadNativeMethod).intercept { chain ->
+ val result = chain.proceed()
+ onUnityLoadNativeAfterHook()
+ result
+ }
startLoop()
}
+ private fun hookMethod(
+ classLoader: ClassLoader,
+ className: String,
+ methodName: String,
+ parameterTypes: Array>,
+ before: ((XposedInterface.Chain) -> Unit)? = null,
+ after: ((XposedInterface.Chain, Any?) -> Unit)? = null,
+ ) {
+ val clazz = classLoader.loadClass(className)
+ val method = clazz.getDeclaredMethod(methodName, *parameterTypes)
+ hook(method).intercept { chain ->
+ before?.invoke(chain)
+ val result = chain.proceed()
+ after?.invoke(chain, result)
+ result
+ }
+ }
+
+ private fun hookAllMethods(clazz: Class<*>, methodName: String, interceptor: (XposedInterface.Chain) -> Any?) {
+ val allMethods = (clazz.declaredMethods.asSequence() + clazz.methods.asSequence())
+ .filter { it.name == methodName }
+ .distinctBy(Method::toGenericString)
+ .toList()
+
+ allMethods.forEach { method ->
+ hook(method).intercept { chain -> interceptor(chain) }
+ }
+ }
+
+ @SuppressLint("UnsafeDynamicallyLoadedCode")
+ private fun onUnityLoadNativeAfterHook() {
+ Log.i(TAG, "UnityPlayer.loadNative")
+
+ if (alreadyInitialized) {
+ return
+ }
+
+ val app = getCurrentApplication()
+ if (app == null) {
+ Log.e(TAG, "currentApplication is null")
+ return
+ }
+
+ if (nativeLibLoadSuccess) {
+ showToast("lib$nativeLibName.so loaded.")
+ } else {
+ showToast("Load native library lib$nativeLibName.so failed.")
+ return
+ }
+
+ if (!gkmsDataInited) {
+ requestConfig(app.applicationContext)
+ }
+
+ FilesChecker.initDir(app.filesDir, modulePath)
+ initHook(
+ "${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
+ File(
+ app.filesDir.absolutePath,
+ FilesChecker.localizationFilesDir
+ ).absolutePath
+ )
+
+ alreadyInitialized = true
+ }
+
+ private fun getCurrentApplication(): Application? {
+ return try {
+ val activityThreadClass = Class.forName("android.app.ActivityThread")
+ val method = activityThreadClass.getDeclaredMethod("currentApplication")
+ method.isAccessible = true
+ method.invoke(null) as? Application
+ } catch (_: Throwable) {
+ null
+ }
+ }
+
@OptIn(DelicateCoroutinesApi::class)
private fun startLoop() {
GlobalScope.launch {
@@ -218,21 +243,20 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
while (isActive) {
val timeTaken = measureTimeMillis {
- val returnValue = pluginCallbackLooper() // plugin main thread loop
+ val returnValue = pluginCallbackLooper()
if (returnValue == 9) {
NativeInitProgress.startInit = true
}
- if (NativeInitProgress.startInit) { // if init, update data
+ if (NativeInitProgress.startInit) {
NativeInitProgress.pluginInitProgressLooper(NativeInitProgress)
gameActivity?.let { initProgressUI.updateData(it) }
}
- if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status
+ if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) {
if (NativeInitProgress.startInit) {
initProgressUI.createView(gameActivity!!)
- }
- else {
+ } else {
initProgressUI.finishLoad(gameActivity!!)
}
}
@@ -269,24 +293,19 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
null
}
- // 清理本地文件
if (programConfig?.cleanLocalAssets == true) {
FilesChecker.cleanAssets()
}
- // 检查 files 版本和 assets 版本并更新
if (programConfig?.checkBuiltInAssets == true) {
FilesChecker.initAndCheck(activity.filesDir, modulePath)
}
- // 强制导出 assets 文件
if (initConfig?.forceExportResource == true) {
FilesChecker.updateFiles()
}
- // 使用热更新文件
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
- // val dataUri = intent.data
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("resource_file", Uri::class.java)
} else {
@@ -297,7 +316,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
if (dataUri != null) {
if (!externalFilesChecked) {
externalFilesChecked = true
- // Log.d(TAG, "dataUri: $dataUri")
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
programConfig.delRemoteAfterUpdate)
}
@@ -305,7 +323,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
else if (programConfig.useAPIAssets) {
if (!File(activity.filesDir, localizationFilesDir).exists() &&
(initConfig?.forceExportResource == false)) {
- // 使用 API 资源,不检查内置,API 资源无效,且游戏内没有插件数据时,释放内置数据
FilesChecker.initAndCheck(activity.filesDir, modulePath)
}
}
@@ -444,10 +461,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
}
- override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
- modulePath = startupParam.modulePath
- }
-
companion object {
@JvmStatic
external fun initHook(targetLibraryPath: String, localizationFilesDir: String)
@@ -468,21 +481,25 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic
external fun loadConfig(configJsonStr: String)
- // Toast快速切换内容
private var toast: Toast? = null
@JvmStatic
fun showToast(message: String) {
- val app = AndroidAppHelper.currentApplication()
+ val app = try {
+ val activityThreadClass = Class.forName("android.app.ActivityThread")
+ val method = activityThreadClass.getDeclaredMethod("currentApplication")
+ method.isAccessible = true
+ method.invoke(null) as? Application
+ } catch (_: Throwable) {
+ null
+ }
+
val context = app?.applicationContext
if (context != null) {
val handler = Handler(Looper.getMainLooper())
handler.post {
- // 取消之前的 Toast
toast?.cancel()
- // 创建新的 Toast
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
- // 展示新的 Toast
toast?.show()
}
}
@@ -494,19 +511,4 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic
external fun pluginCallbackLooper(): Int
}
-
- init {
- ShadowHook.init(
- ConfigBuilder()
- .setMode(ShadowHook.Mode.UNIQUE)
- .build()
- )
-
- nativeLibLoadSuccess = try {
- System.loadLibrary(nativeLibName)
- true
- } catch (e: UnsatisfiedLinkError) {
- false
- }
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt
index ccae7d7..f050bb5 100644
--- a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt
+++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt
@@ -1,15 +1,16 @@
package io.github.chinosk.gakumas.localify.hookUtils
-import android.content.res.XModuleResources
import android.util.Log
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
-
+import java.util.zip.ZipFile
object FilesChecker {
+ private const val MODULE_ASSETS_PREFIX = "assets/"
+
lateinit var filesDir: File
lateinit var modulePath: String
val localizationFilesDir = "gakumas-local"
@@ -17,7 +18,6 @@ object FilesChecker {
fun initAndCheck(fileDir: File, modulePath: String) {
initDir(fileDir, modulePath)
-
checkFiles()
}
@@ -46,31 +46,28 @@ object FilesChecker {
pluginBasePath.mkdirs()
}
- val assets = XModuleResources.createInstance(modulePath, null).assets
- fun forAllAssetFiles(
- basePath: String,
- action: (String, InputStream?) -> Unit
- ) {
- val assetFiles = assets.list(basePath)!!
- for (file in assetFiles) {
- try {
- assets.open("$basePath/$file")
- } catch (e: IOException) {
- action("$basePath/$file", null)
- forAllAssetFiles("$basePath/$file", action)
+ val rootAssetDir = moduleAssetPath(localizationFilesDir).trimEnd('/') + "/"
+ ZipFile(modulePath).use { zipFile ->
+ val entries = zipFile.entries()
+ while (entries.hasMoreElements()) {
+ val entry = entries.nextElement()
+ val name = entry.name
+ if (!name.startsWith(rootAssetDir)) continue
+
+ val relativePath = name.removePrefix(MODULE_ASSETS_PREFIX)
+ if (relativePath.isBlank()) continue
+
+ val outFile = File(filesDir, relativePath)
+ if (entry.isDirectory) {
+ outFile.mkdirs()
continue
- }.use {
- action("$basePath/$file", it)
}
- }
- }
- forAllAssetFiles(localizationFilesDir) { path, file ->
- val outFile = File(filesDir, path)
- if (file == null) {
- outFile.mkdirs()
- } else {
- outFile.outputStream().use { out ->
- file.copyTo(out)
+
+ outFile.parentFile?.mkdirs()
+ zipFile.getInputStream(entry).use { input ->
+ outFile.outputStream().use { output ->
+ input.copyTo(output)
+ }
}
}
}
@@ -79,15 +76,13 @@ object FilesChecker {
}
fun getPluginVersion(): String {
- val assets = XModuleResources.createInstance(modulePath, null).assets
-
- for (i in assets.list(localizationFilesDir)!!) {
- if (i.toString() == "version.txt") {
- val stream = assets.open("$localizationFilesDir/$i")
+ val versionAssetPath = moduleAssetPath("$localizationFilesDir/version.txt")
+ ZipFile(modulePath).use { zipFile ->
+ val entry = zipFile.getEntry(versionAssetPath) ?: return "0.0"
+ zipFile.getInputStream(entry).use { stream ->
return convertToString(stream).trim()
}
}
- return "0.0"
}
fun getInstalledVersion(): String {
@@ -100,26 +95,25 @@ object FilesChecker {
}
fun convertToString(inputStream: InputStream?): String {
- val stringBuilder = StringBuilder()
- var reader: BufferedReader? = null
- try {
- reader = BufferedReader(InputStreamReader(inputStream))
- var line: String?
- while (reader.readLine().also { line = it } != null) {
- stringBuilder.append(line)
+ if (inputStream == null) return ""
+ return try {
+ BufferedReader(InputStreamReader(inputStream)).use { reader ->
+ buildString {
+ var line: String?
+ while (reader.readLine().also { line = it } != null) {
+ append(line)
+ }
+ }
}
} catch (e: IOException) {
e.printStackTrace()
- } finally {
- if (reader != null) {
- try {
- reader.close()
- } catch (e: IOException) {
- e.printStackTrace()
- }
- }
+ ""
}
- return stringBuilder.toString()
+ }
+
+ private fun moduleAssetPath(path: String): String {
+ val cleanPath = path.trimStart('/')
+ return "$MODULE_ASSETS_PREFIX$cleanPath"
}
private fun deleteRecursively(file: File): Boolean {
@@ -167,4 +161,4 @@ object FilesChecker {
i18nFile.writeText("{}")
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/resources/META-INF/xposed/java_init.list b/app/src/main/resources/META-INF/xposed/java_init.list
new file mode 100644
index 0000000..73f145b
--- /dev/null
+++ b/app/src/main/resources/META-INF/xposed/java_init.list
@@ -0,0 +1 @@
+io.github.chinosk.gakumas.localify.GakumasHookMain
diff --git a/app/src/main/resources/META-INF/xposed/module.prop b/app/src/main/resources/META-INF/xposed/module.prop
new file mode 100644
index 0000000..92dd5df
--- /dev/null
+++ b/app/src/main/resources/META-INF/xposed/module.prop
@@ -0,0 +1,3 @@
+minApiVersion=101
+targetApiVersion=101
+staticScope=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 71f1e97..81b60fa 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,7 +6,7 @@ shizukuApi = "12.1.0"
hiddenapi-refine = "4.3.0"
hiddenapi-stub = "4.2.0"
okhttpBom = "4.12.0"
-xposedApi = "82"
+libxposedApi = "101.0.0"
appcompat = "1.7.0"
coil = "2.6.0"
composeBom = "2024.06.00"
@@ -50,7 +50,7 @@ rikka-hidden-stub = { module = "dev.rikka.hidden:stub", version.ref = "hiddenapi
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp = { module = "com.squareup.okhttp3:okhttp" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
-xposed-api = { module = "de.robv.android.xposed:api", version.ref = "xposedApi" }
+libxposed-api = { module = "io.github.libxposed:api", version.ref = "libxposedApi" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
material = { module = "com.google.android.material:material", version.ref = "material" }
diff --git a/settings.gradle b/settings.gradle
index cef7bc5..ebc0828 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -18,7 +18,6 @@ dependencyResolutionManagement {
google()
mavenCentral()
- maven { url "https://api.xposed.info/" }
}
}