forked from chinosk/gkms-local
Compare commits
2 Commits
c7af3e41a5
...
a12651d60f
| Author | SHA1 | Date |
|---|---|---|
|
|
a12651d60f | |
|
|
048827feee |
|
|
@ -130,6 +130,6 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.xdl)
|
implementation(libs.xdl)
|
||||||
implementation(libs.shadowhook)
|
implementation(libs.shadowhook)
|
||||||
compileOnly(libs.xposed.api)
|
compileOnly(libs.libxposed.api)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
}
|
}
|
||||||
|
|
@ -21,23 +21,6 @@
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedmodule"
|
|
||||||
android:value="true" />
|
|
||||||
<meta-data
|
|
||||||
android:name="xposeddescription"
|
|
||||||
android:value="IDOLM@STER Gakuen localify" />
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedminversion"
|
|
||||||
android:value="53" />
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedsharedprefs"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedscope"
|
|
||||||
android:value="com.bandainamcoent.idolmaster_gakuen" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
@ -101,4 +84,4 @@
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
io.github.chinosk.gakumas.localify.GakumasHookMain
|
|
||||||
|
|
@ -3,7 +3,7 @@ package io.github.chinosk.gakumas.localify
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.AndroidAppHelper
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
|
@ -17,34 +17,32 @@ import android.view.MotionEvent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.bytedance.shadowhook.ShadowHook
|
import com.bytedance.shadowhook.ShadowHook
|
||||||
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
|
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
|
||||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
|
||||||
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.FilesChecker
|
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.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.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.lang.reflect.Method
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.system.measureTimeMillis
|
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"
|
val TAG = "GakumasLocalify"
|
||||||
|
|
||||||
class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
class GakumasHookMain : XposedModule() {
|
||||||
private lateinit var modulePath: String
|
private var modulePath: String = ""
|
||||||
private var nativeLibLoadSuccess: Boolean
|
private var nativeLibLoadSuccess: Boolean = false
|
||||||
private var alreadyInitialized = false
|
private var alreadyInitialized = false
|
||||||
private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen"
|
private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen"
|
||||||
private val nativeLibName = "MarryKotone"
|
private val nativeLibName = "MarryKotone"
|
||||||
|
|
@ -55,160 +53,187 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
private var externalFilesChecked: Boolean = false
|
private var externalFilesChecked: Boolean = false
|
||||||
private var gameActivity: Activity? = null
|
private var gameActivity: Activity? = null
|
||||||
|
|
||||||
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
|
override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
|
||||||
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
|
modulePath = getModuleApplicationInfo().sourceDir
|
||||||
// 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}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XposedHelpers.findAndHookMethod(
|
val classLoader = param.classLoader
|
||||||
"android.app.Activity",
|
|
||||||
lpparam.classLoader,
|
hookMethod(
|
||||||
"dispatchKeyEvent",
|
classLoader = classLoader,
|
||||||
KeyEvent::class.java,
|
className = "android.app.Activity",
|
||||||
object : XC_MethodHook() {
|
methodName = "dispatchKeyEvent",
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
parameterTypes = arrayOf(KeyEvent::class.java),
|
||||||
val keyEvent = param.args[0] as KeyEvent
|
before = { chain ->
|
||||||
val keyCode = keyEvent.keyCode
|
val keyEvent = chain.getArg(0) as KeyEvent
|
||||||
val action = keyEvent.action
|
keyboardEvent(keyEvent.keyCode, keyEvent.action)
|
||||||
// Log.d(TAG, "Key event: keyCode=$keyCode, action=$action")
|
|
||||||
keyboardEvent(keyCode, action)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
XposedHelpers.findAndHookMethod(
|
hookMethod(
|
||||||
"android.app.Activity",
|
classLoader = classLoader,
|
||||||
lpparam.classLoader,
|
className = "android.app.Activity",
|
||||||
"dispatchGenericMotionEvent",
|
methodName = "dispatchGenericMotionEvent",
|
||||||
MotionEvent::class.java,
|
parameterTypes = arrayOf(MotionEvent::class.java),
|
||||||
object : XC_MethodHook() {
|
before = { chain ->
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
val motionEvent = chain.getArg(0) as MotionEvent
|
||||||
val motionEvent = param.args[0] as MotionEvent
|
val action = motionEvent.action
|
||||||
val action = motionEvent.action
|
|
||||||
|
|
||||||
// 左摇杆的X和Y轴
|
val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
|
||||||
val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
|
val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
|
||||||
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轴
|
joystickEvent(
|
||||||
val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
|
action,
|
||||||
val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
|
leftStickX,
|
||||||
|
leftStickY,
|
||||||
// 左扳机
|
rightStickX,
|
||||||
val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
|
rightStickY,
|
||||||
|
leftTrigger,
|
||||||
// 右扳机
|
rightTrigger,
|
||||||
val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
|
hatX,
|
||||||
|
hatY
|
||||||
// 十字键
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader)
|
val activityClass = classLoader.loadClass("android.app.Activity")
|
||||||
XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() {
|
hookAllMethods(activityClass, "onStart") { chain ->
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
Log.d(TAG, "onStart")
|
||||||
super.beforeHookedMethod(param)
|
val currActivity = chain.thisObject as Activity
|
||||||
Log.d(TAG, "onStart")
|
gameActivity = currActivity
|
||||||
val currActivity = param.thisObject as Activity
|
if (getConfigError != null) {
|
||||||
gameActivity = currActivity
|
showGetConfigFailed(currActivity)
|
||||||
if (getConfigError != null) {
|
} else {
|
||||||
showGetConfigFailed(currActivity)
|
initGkmsConfig(currActivity)
|
||||||
}
|
|
||||||
else {
|
|
||||||
initGkmsConfig(currActivity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
chain.proceed()
|
||||||
|
}
|
||||||
|
|
||||||
XposedBridge.hookAllMethods(appActivityClass, "onResume", object : XC_MethodHook() {
|
hookAllMethods(activityClass, "onResume") { chain ->
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
Log.d(TAG, "onResume")
|
||||||
Log.d(TAG, "onResume")
|
val currActivity = chain.thisObject as Activity
|
||||||
val currActivity = param.thisObject as Activity
|
gameActivity = currActivity
|
||||||
gameActivity = currActivity
|
if (getConfigError != null) {
|
||||||
if (getConfigError != null) {
|
showGetConfigFailed(currActivity)
|
||||||
showGetConfigFailed(currActivity)
|
} else {
|
||||||
}
|
initGkmsConfig(currActivity)
|
||||||
else {
|
|
||||||
initGkmsConfig(currActivity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
chain.proceed()
|
||||||
|
}
|
||||||
|
|
||||||
val cls = lpparam.classLoader.loadClass("com.unity3d.player.UnityPlayer")
|
val unityPlayerClass = classLoader.loadClass("com.unity3d.player.UnityPlayer")
|
||||||
XposedHelpers.findAndHookMethod(
|
val loadNativeMethod = unityPlayerClass.getDeclaredMethod("loadNative", String::class.java)
|
||||||
cls,
|
|
||||||
"loadNative",
|
|
||||||
String::class.java,
|
|
||||||
object : XC_MethodHook() {
|
|
||||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
|
||||||
override fun afterHookedMethod(param: MethodHookParam) {
|
|
||||||
super.afterHookedMethod(param)
|
|
||||||
|
|
||||||
Log.i(TAG, "UnityPlayer.loadNative")
|
hook(loadNativeMethod).intercept { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
if (alreadyInitialized) {
|
onUnityLoadNativeAfterHook()
|
||||||
return
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
startLoop()
|
startLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hookMethod(
|
||||||
|
classLoader: ClassLoader,
|
||||||
|
className: String,
|
||||||
|
methodName: String,
|
||||||
|
parameterTypes: Array<Class<*>>,
|
||||||
|
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)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private fun startLoop() {
|
private fun startLoop() {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
|
|
@ -218,21 +243,20 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val timeTaken = measureTimeMillis {
|
val timeTaken = measureTimeMillis {
|
||||||
val returnValue = pluginCallbackLooper() // plugin main thread loop
|
val returnValue = pluginCallbackLooper()
|
||||||
if (returnValue == 9) {
|
if (returnValue == 9) {
|
||||||
NativeInitProgress.startInit = true
|
NativeInitProgress.startInit = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NativeInitProgress.startInit) { // if init, update data
|
if (NativeInitProgress.startInit) {
|
||||||
NativeInitProgress.pluginInitProgressLooper(NativeInitProgress)
|
NativeInitProgress.pluginInitProgressLooper(NativeInitProgress)
|
||||||
gameActivity?.let { initProgressUI.updateData(it) }
|
gameActivity?.let { initProgressUI.updateData(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status
|
if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) {
|
||||||
if (NativeInitProgress.startInit) {
|
if (NativeInitProgress.startInit) {
|
||||||
initProgressUI.createView(gameActivity!!)
|
initProgressUI.createView(gameActivity!!)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
initProgressUI.finishLoad(gameActivity!!)
|
initProgressUI.finishLoad(gameActivity!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,24 +293,19 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理本地文件
|
|
||||||
if (programConfig?.cleanLocalAssets == true) {
|
if (programConfig?.cleanLocalAssets == true) {
|
||||||
FilesChecker.cleanAssets()
|
FilesChecker.cleanAssets()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 files 版本和 assets 版本并更新
|
|
||||||
if (programConfig?.checkBuiltInAssets == true) {
|
if (programConfig?.checkBuiltInAssets == true) {
|
||||||
FilesChecker.initAndCheck(activity.filesDir, modulePath)
|
FilesChecker.initAndCheck(activity.filesDir, modulePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制导出 assets 文件
|
|
||||||
if (initConfig?.forceExportResource == true) {
|
if (initConfig?.forceExportResource == true) {
|
||||||
FilesChecker.updateFiles()
|
FilesChecker.updateFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用热更新文件
|
|
||||||
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
|
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
|
||||||
// val dataUri = intent.data
|
|
||||||
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
intent.getParcelableExtra("resource_file", Uri::class.java)
|
intent.getParcelableExtra("resource_file", Uri::class.java)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -297,7 +316,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
if (dataUri != null) {
|
if (dataUri != null) {
|
||||||
if (!externalFilesChecked) {
|
if (!externalFilesChecked) {
|
||||||
externalFilesChecked = true
|
externalFilesChecked = true
|
||||||
// Log.d(TAG, "dataUri: $dataUri")
|
|
||||||
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
|
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
|
||||||
programConfig.delRemoteAfterUpdate)
|
programConfig.delRemoteAfterUpdate)
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +323,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
else if (programConfig.useAPIAssets) {
|
else if (programConfig.useAPIAssets) {
|
||||||
if (!File(activity.filesDir, localizationFilesDir).exists() &&
|
if (!File(activity.filesDir, localizationFilesDir).exists() &&
|
||||||
(initConfig?.forceExportResource == false)) {
|
(initConfig?.forceExportResource == false)) {
|
||||||
// 使用 API 资源,不检查内置,API 资源无效,且游戏内没有插件数据时,释放内置数据
|
|
||||||
FilesChecker.initAndCheck(activity.filesDir, modulePath)
|
FilesChecker.initAndCheck(activity.filesDir, modulePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -444,10 +461,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
|
|
||||||
modulePath = startupParam.modulePath
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun initHook(targetLibraryPath: String, localizationFilesDir: String)
|
external fun initHook(targetLibraryPath: String, localizationFilesDir: String)
|
||||||
|
|
@ -468,21 +481,25 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun loadConfig(configJsonStr: String)
|
external fun loadConfig(configJsonStr: String)
|
||||||
|
|
||||||
// Toast快速切换内容
|
|
||||||
private var toast: Toast? = null
|
private var toast: Toast? = null
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun showToast(message: String) {
|
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
|
val context = app?.applicationContext
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
handler.post {
|
handler.post {
|
||||||
// 取消之前的 Toast
|
|
||||||
toast?.cancel()
|
toast?.cancel()
|
||||||
// 创建新的 Toast
|
|
||||||
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||||
// 展示新的 Toast
|
|
||||||
toast?.show()
|
toast?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -494,19 +511,4 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun pluginCallbackLooper(): Int
|
external fun pluginCallbackLooper(): Int
|
||||||
}
|
}
|
||||||
|
}
|
||||||
init {
|
|
||||||
ShadowHook.init(
|
|
||||||
ConfigBuilder()
|
|
||||||
.setMode(ShadowHook.Mode.UNIQUE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
nativeLibLoadSuccess = try {
|
|
||||||
System.loadLibrary(nativeLibName)
|
|
||||||
true
|
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,11 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
|
||||||
}
|
}
|
||||||
|
|
||||||
fun gotoPatchActivity() {
|
fun gotoPatchActivity() {
|
||||||
val intent = Intent(this, PatchActivity::class.java)
|
mainUIConfirmStatUpdate(
|
||||||
startActivity(intent)
|
isShow = true,
|
||||||
|
title = getString(R.string.patcher_unavailable_title),
|
||||||
|
content = getString(R.string.patcher_unavailable_content)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveConfig() {
|
override fun saveConfig() {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
package io.github.chinosk.gakumas.localify.hookUtils
|
package io.github.chinosk.gakumas.localify.hookUtils
|
||||||
|
|
||||||
import android.content.res.XModuleResources
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
object FilesChecker {
|
object FilesChecker {
|
||||||
|
private const val MODULE_ASSETS_PREFIX = "assets/"
|
||||||
|
|
||||||
lateinit var filesDir: File
|
lateinit var filesDir: File
|
||||||
lateinit var modulePath: String
|
lateinit var modulePath: String
|
||||||
val localizationFilesDir = "gakumas-local"
|
val localizationFilesDir = "gakumas-local"
|
||||||
|
|
@ -17,7 +18,6 @@ object FilesChecker {
|
||||||
|
|
||||||
fun initAndCheck(fileDir: File, modulePath: String) {
|
fun initAndCheck(fileDir: File, modulePath: String) {
|
||||||
initDir(fileDir, modulePath)
|
initDir(fileDir, modulePath)
|
||||||
|
|
||||||
checkFiles()
|
checkFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,31 +46,28 @@ object FilesChecker {
|
||||||
pluginBasePath.mkdirs()
|
pluginBasePath.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
val assets = XModuleResources.createInstance(modulePath, null).assets
|
val rootAssetDir = moduleAssetPath(localizationFilesDir).trimEnd('/') + "/"
|
||||||
fun forAllAssetFiles(
|
ZipFile(modulePath).use { zipFile ->
|
||||||
basePath: String,
|
val entries = zipFile.entries()
|
||||||
action: (String, InputStream?) -> Unit
|
while (entries.hasMoreElements()) {
|
||||||
) {
|
val entry = entries.nextElement()
|
||||||
val assetFiles = assets.list(basePath)!!
|
val name = entry.name
|
||||||
for (file in assetFiles) {
|
if (!name.startsWith(rootAssetDir)) continue
|
||||||
try {
|
|
||||||
assets.open("$basePath/$file")
|
val relativePath = name.removePrefix(MODULE_ASSETS_PREFIX)
|
||||||
} catch (e: IOException) {
|
if (relativePath.isBlank()) continue
|
||||||
action("$basePath/$file", null)
|
|
||||||
forAllAssetFiles("$basePath/$file", action)
|
val outFile = File(filesDir, relativePath)
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
outFile.mkdirs()
|
||||||
continue
|
continue
|
||||||
}.use {
|
|
||||||
action("$basePath/$file", it)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
outFile.parentFile?.mkdirs()
|
||||||
forAllAssetFiles(localizationFilesDir) { path, file ->
|
zipFile.getInputStream(entry).use { input ->
|
||||||
val outFile = File(filesDir, path)
|
outFile.outputStream().use { output ->
|
||||||
if (file == null) {
|
input.copyTo(output)
|
||||||
outFile.mkdirs()
|
}
|
||||||
} else {
|
|
||||||
outFile.outputStream().use { out ->
|
|
||||||
file.copyTo(out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,15 +76,13 @@ object FilesChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPluginVersion(): String {
|
fun getPluginVersion(): String {
|
||||||
val assets = XModuleResources.createInstance(modulePath, null).assets
|
val versionAssetPath = moduleAssetPath("$localizationFilesDir/version.txt")
|
||||||
|
ZipFile(modulePath).use { zipFile ->
|
||||||
for (i in assets.list(localizationFilesDir)!!) {
|
val entry = zipFile.getEntry(versionAssetPath) ?: return "0.0"
|
||||||
if (i.toString() == "version.txt") {
|
zipFile.getInputStream(entry).use { stream ->
|
||||||
val stream = assets.open("$localizationFilesDir/$i")
|
|
||||||
return convertToString(stream).trim()
|
return convertToString(stream).trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "0.0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getInstalledVersion(): String {
|
fun getInstalledVersion(): String {
|
||||||
|
|
@ -100,26 +95,25 @@ object FilesChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertToString(inputStream: InputStream?): String {
|
fun convertToString(inputStream: InputStream?): String {
|
||||||
val stringBuilder = StringBuilder()
|
if (inputStream == null) return ""
|
||||||
var reader: BufferedReader? = null
|
return try {
|
||||||
try {
|
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||||
reader = BufferedReader(InputStreamReader(inputStream))
|
buildString {
|
||||||
var line: String?
|
var line: String?
|
||||||
while (reader.readLine().also { line = it } != null) {
|
while (reader.readLine().also { line = it } != null) {
|
||||||
stringBuilder.append(line)
|
append(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
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 {
|
private fun deleteRecursively(file: File): Boolean {
|
||||||
|
|
@ -167,4 +161,4 @@ object FilesChecker {
|
||||||
i18nFile.writeText("{}")
|
i18nFile.writeText("{}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,8 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
|
||||||
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string>
|
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string>
|
||||||
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。
|
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。
|
||||||
個人データのバックアップを設定済みであることを確認してください。"</string>
|
個人データのバックアップを設定済みであることを確認してください。"</string>
|
||||||
|
<string name="patcher_unavailable_title">Patcher は利用できません</string>
|
||||||
|
<string name="patcher_unavailable_content">このモジュールは新しい Xposed API に移行したため、埋め込みパッチモードは現在サポートしていません。</string>
|
||||||
<string name="path_password_eye">M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z</string>
|
<string name="path_password_eye">M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z</string>
|
||||||
<string name="path_password_eye_mask_strike_through">M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
|
<string name="path_password_eye_mask_strike_through">M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
|
||||||
<string name="path_password_eye_mask_visible">M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
|
<string name="path_password_eye_mask_visible">M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,9 @@
|
||||||
<string name="patch_uninstall_text">由于签名不同,安装修补的应用前需要先卸载原应用。\n确保您已备份好个人数据。</string>
|
<string name="patch_uninstall_text">由于签名不同,安装修补的应用前需要先卸载原应用。\n确保您已备份好个人数据。</string>
|
||||||
<string name="patch_uninstall_confirm">您确定要卸载吗</string>
|
<string name="patch_uninstall_confirm">您确定要卸载吗</string>
|
||||||
<string name="patch_finished">修补完成,是否开始安装?</string>
|
<string name="patch_finished">修补完成,是否开始安装?</string>
|
||||||
|
<string name="patcher_unavailable_title">Patcher 暂不可用</string>
|
||||||
|
<string name="patcher_unavailable_content">该模块已迁移到新版 Xposed API,暂不支持内嵌修补模式。</string>
|
||||||
|
|
||||||
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
|
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
|
||||||
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
|
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,9 @@
|
||||||
<string name="patch_uninstall_text">Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data.</string>
|
<string name="patch_uninstall_text">Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data.</string>
|
||||||
<string name="patch_uninstall_confirm">Are you sure you want to uninstall?</string>
|
<string name="patch_uninstall_confirm">Are you sure you want to uninstall?</string>
|
||||||
<string name="patch_finished">Patch finished. Start installing?</string>
|
<string name="patch_finished">Patch finished. Start installing?</string>
|
||||||
|
<string name="patcher_unavailable_title">Patcher Unavailable</string>
|
||||||
|
<string name="patcher_unavailable_content">This module has migrated to the modern Xposed API. Embedded patch mode is temporarily unsupported.</string>
|
||||||
|
|
||||||
<string name="about_contributors_asset_file">about_contributors_en.json</string>
|
<string name="about_contributors_asset_file">about_contributors_en.json</string>
|
||||||
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
|
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
io.github.chinosk.gakumas.localify.GakumasHookMain
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
minApiVersion=101
|
||||||
|
targetApiVersion=101
|
||||||
|
staticScope=false
|
||||||
|
|
@ -6,7 +6,7 @@ shizukuApi = "12.1.0"
|
||||||
hiddenapi-refine = "4.3.0"
|
hiddenapi-refine = "4.3.0"
|
||||||
hiddenapi-stub = "4.2.0"
|
hiddenapi-stub = "4.2.0"
|
||||||
okhttpBom = "4.12.0"
|
okhttpBom = "4.12.0"
|
||||||
xposedApi = "82"
|
libxposedApi = "101.0.0"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
composeBom = "2024.06.00"
|
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" }
|
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||||
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
|
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-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
|
||||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ dependencyResolutionManagement {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
||||||
maven { url "https://api.xposed.info/" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue