Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

18 changed files with 254 additions and 393 deletions

View File

@ -2,8 +2,6 @@
- 学园偶像大师 本地化插件 - 学园偶像大师 本地化插件
- **开发中** - **开发中**
- 下游更改将API版本提升至101并暂时禁用Patcher

View File

@ -130,6 +130,6 @@ dependencies {
implementation(libs.xdl) implementation(libs.xdl)
implementation(libs.shadowhook) implementation(libs.shadowhook)
compileOnly(libs.libxposed.api) compileOnly(libs.xposed.api)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
} }

View File

@ -21,6 +21,23 @@
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"
@ -84,4 +101,4 @@
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1 @@
io.github.chinosk.gakumas.localify.GakumasHookMain

View File

@ -68,8 +68,4 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
log log
fmt) fmt)
if (ANDROID AND ANDROID_PLATFORM_LEVEL GREATER_EQUAL 31)
target_link_libraries(${CMAKE_PROJECT_NAME} icu)
endif()
target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_23) target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_23)

View File

@ -1,12 +1,11 @@
#include "Misc.hpp" #include "Misc.hpp"
#include <codecvt>
#include <locale>
#include "fmt/core.h" #include "fmt/core.h"
#ifndef GKMS_WINDOWS #ifndef GKMS_WINDOWS
#include <jni.h> #include <jni.h>
#if defined(__ANDROID__) && __ANDROID_API__ >= 31
#include <unicode/ustring.h>
#endif
extern JavaVM* g_javaVM; extern JavaVM* g_javaVM;
#else #else
@ -33,136 +32,14 @@ namespace GakumasLocal::Misc {
return utility::conversions::utf16_to_utf8(wstr); return utility::conversions::utf16_to_utf8(wstr);
} }
#else #else
namespace {
jclass GetStringClass(JNIEnv* env) {
static jclass stringClass = nullptr;
if (stringClass) return stringClass;
jclass localClass = env->FindClass("java/lang/String");
if (!localClass) return nullptr;
stringClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
env->DeleteLocalRef(localClass);
return stringClass;
}
jstring GetUtf8CharsetName(JNIEnv* env) {
static jstring utf8Charset = nullptr;
if (utf8Charset) return utf8Charset;
jstring localUtf8 = env->NewStringUTF("UTF-8");
if (!localUtf8) return nullptr;
utf8Charset = reinterpret_cast<jstring>(env->NewGlobalRef(localUtf8));
env->DeleteLocalRef(localUtf8);
return utf8Charset;
}
}
std::u16string ToUTF16(const std::string_view& str) { std::u16string ToUTF16(const std::string_view& str) {
#if defined(__ANDROID__) && __ANDROID_API__ >= 31 std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv;
UErrorCode status = U_ZERO_ERROR; return utf16conv.from_bytes(str.data(), str.data() + str.size());
int32_t outLen = 0;
u_strFromUTF8(nullptr, 0, &outLen, str.data(), static_cast<int32_t>(str.size()), &status);
if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) return {};
status = U_ZERO_ERROR;
std::u16string out(outLen, u'\0');
u_strFromUTF8(
reinterpret_cast<UChar*>(out.data()),
outLen,
&outLen,
str.data(),
static_cast<int32_t>(str.size()),
&status);
if (U_FAILURE(status)) return {};
out.resize(outLen);
return out;
#else
JNIEnv* env = GetJNIEnv();
if (!env) return {};
jclass stringClass = GetStringClass(env);
jstring utf8Charset = GetUtf8CharsetName(env);
if (!stringClass || !utf8Charset) return {};
jmethodID ctor = env->GetMethodID(stringClass, "<init>", "([BLjava/lang/String;)V");
if (!ctor) return {};
jbyteArray bytes = env->NewByteArray(static_cast<jsize>(str.size()));
if (!bytes) return {};
env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(str.size()), reinterpret_cast<const jbyte*>(str.data()));
jstring jstr = reinterpret_cast<jstring>(env->NewObject(stringClass, ctor, bytes, utf8Charset));
env->DeleteLocalRef(bytes);
if (!jstr || env->ExceptionCheck()) {
env->ExceptionClear();
if (jstr) env->DeleteLocalRef(jstr);
return {};
}
const jsize len = env->GetStringLength(jstr);
const jchar* chars = env->GetStringChars(jstr, nullptr);
if (!chars) {
env->DeleteLocalRef(jstr);
return {};
}
std::u16string out(reinterpret_cast<const char16_t*>(chars), reinterpret_cast<const char16_t*>(chars) + len);
env->ReleaseStringChars(jstr, chars);
env->DeleteLocalRef(jstr);
return out;
#endif
} }
std::string ToUTF8(const std::u16string_view& str) { std::string ToUTF8(const std::u16string_view& str) {
#if defined(__ANDROID__) && __ANDROID_API__ >= 31 std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv;
UErrorCode status = U_ZERO_ERROR; return utf16conv.to_bytes(str.data(), str.data() + str.size());
int32_t outLen = 0;
u_strToUTF8(nullptr, 0, &outLen, reinterpret_cast<const UChar*>(str.data()), static_cast<int32_t>(str.size()), &status);
if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) return {};
status = U_ZERO_ERROR;
std::string out(outLen, '\0');
u_strToUTF8(
out.data(),
outLen,
&outLen,
reinterpret_cast<const UChar*>(str.data()),
static_cast<int32_t>(str.size()),
&status);
if (U_FAILURE(status)) return {};
out.resize(outLen);
return out;
#else
JNIEnv* env = GetJNIEnv();
if (!env) return {};
jclass stringClass = GetStringClass(env);
jstring utf8Charset = GetUtf8CharsetName(env);
if (!stringClass || !utf8Charset) return {};
jstring jstr = env->NewString(reinterpret_cast<const jchar*>(str.data()), static_cast<jsize>(str.size()));
if (!jstr) return {};
jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B");
if (!getBytes) {
env->DeleteLocalRef(jstr);
return {};
}
jbyteArray bytes = reinterpret_cast<jbyteArray>(env->CallObjectMethod(jstr, getBytes, utf8Charset));
env->DeleteLocalRef(jstr);
if (!bytes || env->ExceptionCheck()) {
env->ExceptionClear();
if (bytes) env->DeleteLocalRef(bytes);
return {};
}
const jsize len = env->GetArrayLength(bytes);
std::string out(static_cast<size_t>(len), '\0');
env->GetByteArrayRegion(bytes, 0, len, reinterpret_cast<jbyte*>(out.data()));
env->DeleteLocalRef(bytes);
return out;
#endif
} }
#endif #endif

View File

@ -565,10 +565,10 @@ public:
} }
catch (...) { catch (...) {
std::cout << funcName << " Invoke Error\n"; std::cout << funcName << " Invoke Error\n";
return Return(); Return();
} }
} }
return Return(); Return();
} }
inline static std::vector<Assembly*> assembly; inline static std::vector<Assembly*> assembly;

View File

@ -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.Application import android.app.AndroidAppHelper
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,32 +17,34 @@ 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 io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater 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.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 : XposedModule() { class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var modulePath: String = "" private lateinit var modulePath: String
private var nativeLibLoadSuccess: Boolean = false private var nativeLibLoadSuccess: Boolean
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"
@ -53,197 +55,160 @@ class GakumasHookMain : XposedModule() {
private var externalFilesChecked: Boolean = false private var externalFilesChecked: Boolean = false
private var gameActivity: Activity? = null private var gameActivity: Activity? = null
override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) { override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
modulePath = getModuleApplicationInfo().sourceDir // 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}")
// }
// }
// )
// }
ShadowHook.init( if (lpparam.packageName != targetPackageName) {
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
} }
val classLoader = param.classLoader XposedHelpers.findAndHookMethod(
"android.app.Activity",
hookMethod( lpparam.classLoader,
classLoader = classLoader, "dispatchKeyEvent",
className = "android.app.Activity", KeyEvent::class.java,
methodName = "dispatchKeyEvent", object : XC_MethodHook() {
parameterTypes = arrayOf(KeyEvent::class.java), override fun beforeHookedMethod(param: MethodHookParam) {
before = { chain -> val keyEvent = param.args[0] as KeyEvent
val keyEvent = chain.getArg(0) as KeyEvent val keyCode = keyEvent.keyCode
keyboardEvent(keyEvent.keyCode, keyEvent.action) val action = keyEvent.action
// Log.d(TAG, "Key event: keyCode=$keyCode, action=$action")
keyboardEvent(keyCode, action)
}
} }
) )
hookMethod( XposedHelpers.findAndHookMethod(
classLoader = classLoader, "android.app.Activity",
className = "android.app.Activity", lpparam.classLoader,
methodName = "dispatchGenericMotionEvent", "dispatchGenericMotionEvent",
parameterTypes = arrayOf(MotionEvent::class.java), MotionEvent::class.java,
before = { chain -> object : XC_MethodHook() {
val motionEvent = chain.getArg(0) as MotionEvent override fun beforeHookedMethod(param: MethodHookParam) {
val action = motionEvent.action val motionEvent = param.args[0] as MotionEvent
val action = motionEvent.action
// 左摇杆的X和Y轴 // 左摇杆的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)
// 右摇杆的X和Y轴 // 右摇杆的X和Y轴
val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z) val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ) val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
// 左扳机 // 左扳机
val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER) val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
// 右扳机 // 右扳机
val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER) val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
// 十字键 // 十字键
val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X) val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y) val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
// 处理摇杆和扳机事件 // 处理摇杆和扳机事件
joystickEvent( joystickEvent(
action, action,
leftStickX, leftStickX,
leftStickY, leftStickY,
rightStickX, rightStickX,
rightStickY, rightStickY,
leftTrigger, leftTrigger,
rightTrigger, rightTrigger,
hatX, hatX,
hatY hatY
) )
}
} }
) )
val activityClass = classLoader.loadClass("android.app.Activity") val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader)
hookAllMethods(activityClass, "onStart") { chain -> XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() {
Log.d(TAG, "onStart") override fun beforeHookedMethod(param: MethodHookParam) {
val currActivity = chain.thisObject as Activity super.beforeHookedMethod(param)
gameActivity = currActivity Log.d(TAG, "onStart")
if (getConfigError != null) { val currActivity = param.thisObject as Activity
showGetConfigFailed(currActivity) gameActivity = currActivity
} else { if (getConfigError != null) {
initGkmsConfig(currActivity) showGetConfigFailed(currActivity)
}
else {
initGkmsConfig(currActivity)
}
} }
chain.proceed() })
}
hookAllMethods(activityClass, "onResume") { chain -> XposedBridge.hookAllMethods(appActivityClass, "onResume", object : XC_MethodHook() {
Log.d(TAG, "onResume") override fun beforeHookedMethod(param: MethodHookParam) {
val currActivity = chain.thisObject as Activity Log.d(TAG, "onResume")
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() })
}
val unityPlayerClass = classLoader.loadClass("com.unity3d.player.UnityPlayer") val cls = lpparam.classLoader.loadClass("com.unity3d.player.UnityPlayer")
val loadNativeMethod = unityPlayerClass.getDeclaredMethod("loadNative", String::class.java) XposedHelpers.findAndHookMethod(
cls,
"loadNative",
String::class.java,
object : XC_MethodHook() {
@SuppressLint("UnsafeDynamicallyLoadedCode")
override fun afterHookedMethod(param: MethodHookParam) {
super.afterHookedMethod(param)
hook(loadNativeMethod).intercept { chain -> Log.i(TAG, "UnityPlayer.loadNative")
val result = chain.proceed()
onUnityLoadNativeAfterHook() if (alreadyInitialized) {
result 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
}
})
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 {
@ -266,7 +231,8 @@ class GakumasHookMain : XposedModule() {
if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status
if (NativeInitProgress.startInit) { if (NativeInitProgress.startInit) {
initProgressUI.createView(gameActivity!!) initProgressUI.createView(gameActivity!!)
} else { }
else {
initProgressUI.finishLoad(gameActivity!!) initProgressUI.finishLoad(gameActivity!!)
} }
} }
@ -320,6 +286,7 @@ class GakumasHookMain : XposedModule() {
// 使用热更新文件 // 使用热更新文件
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 {
@ -330,6 +297,7 @@ class GakumasHookMain : XposedModule() {
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)
} }
@ -476,6 +444,10 @@ class GakumasHookMain : XposedModule() {
} }
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)
@ -501,15 +473,7 @@ class GakumasHookMain : XposedModule() {
@JvmStatic @JvmStatic
fun showToast(message: String) { fun showToast(message: String) {
val app = try { val app = AndroidAppHelper.currentApplication()
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())
@ -530,4 +494,19 @@ class GakumasHookMain : XposedModule() {
@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
}
}
}

View File

@ -48,11 +48,8 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
} }
fun gotoPatchActivity() { fun gotoPatchActivity() {
mainUIConfirmStatUpdate( val intent = Intent(this, PatchActivity::class.java)
isShow = true, startActivity(intent)
title = getString(R.string.patcher_unavailable_title),
content = getString(R.string.patcher_unavailable_content)
)
} }
override fun saveConfig() { override fun saveConfig() {

View File

@ -1,16 +1,15 @@
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"
@ -18,6 +17,7 @@ object FilesChecker {
fun initAndCheck(fileDir: File, modulePath: String) { fun initAndCheck(fileDir: File, modulePath: String) {
initDir(fileDir, modulePath) initDir(fileDir, modulePath)
checkFiles() checkFiles()
} }
@ -46,28 +46,31 @@ object FilesChecker {
pluginBasePath.mkdirs() pluginBasePath.mkdirs()
} }
val rootAssetDir = moduleAssetPath(localizationFilesDir).trimEnd('/') + "/" val assets = XModuleResources.createInstance(modulePath, null).assets
ZipFile(modulePath).use { zipFile -> fun forAllAssetFiles(
val entries = zipFile.entries() basePath: String,
while (entries.hasMoreElements()) { action: (String, InputStream?) -> Unit
val entry = entries.nextElement() ) {
val name = entry.name val assetFiles = assets.list(basePath)!!
if (!name.startsWith(rootAssetDir)) continue for (file in assetFiles) {
try {
val relativePath = name.removePrefix(MODULE_ASSETS_PREFIX) assets.open("$basePath/$file")
if (relativePath.isBlank()) continue } catch (e: IOException) {
action("$basePath/$file", null)
val outFile = File(filesDir, relativePath) forAllAssetFiles("$basePath/$file", action)
if (entry.isDirectory) {
outFile.mkdirs()
continue continue
}.use {
action("$basePath/$file", it)
} }
}
outFile.parentFile?.mkdirs() }
zipFile.getInputStream(entry).use { input -> forAllAssetFiles(localizationFilesDir) { path, file ->
outFile.outputStream().use { output -> val outFile = File(filesDir, path)
input.copyTo(output) if (file == null) {
} outFile.mkdirs()
} else {
outFile.outputStream().use { out ->
file.copyTo(out)
} }
} }
} }
@ -76,13 +79,15 @@ object FilesChecker {
} }
fun getPluginVersion(): String { fun getPluginVersion(): String {
val versionAssetPath = moduleAssetPath("$localizationFilesDir/version.txt") val assets = XModuleResources.createInstance(modulePath, null).assets
ZipFile(modulePath).use { zipFile ->
val entry = zipFile.getEntry(versionAssetPath) ?: return "0.0" for (i in assets.list(localizationFilesDir)!!) {
zipFile.getInputStream(entry).use { stream -> if (i.toString() == "version.txt") {
val stream = assets.open("$localizationFilesDir/$i")
return convertToString(stream).trim() return convertToString(stream).trim()
} }
} }
return "0.0"
} }
fun getInstalledVersion(): String { fun getInstalledVersion(): String {
@ -95,25 +100,26 @@ object FilesChecker {
} }
fun convertToString(inputStream: InputStream?): String { fun convertToString(inputStream: InputStream?): String {
if (inputStream == null) return "" val stringBuilder = StringBuilder()
return try { var reader: BufferedReader? = null
BufferedReader(InputStreamReader(inputStream)).use { reader -> try {
buildString { reader = BufferedReader(InputStreamReader(inputStream))
var line: String? var line: String?
while (reader.readLine().also { line = it } != null) { while (reader.readLine().also { line = it } != null) {
append(line) stringBuilder.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 {
@ -161,4 +167,4 @@ object FilesChecker {
i18nFile.writeText("{}") i18nFile.writeText("{}")
} }
} }
} }

View File

@ -299,8 +299,6 @@ 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">GKMSPatchを使用して、root権限なしでご利用ください</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>

View File

@ -102,9 +102,7 @@
<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">请使用GKMSPatch进行免Root使用</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>

View File

@ -102,9 +102,7 @@
<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">Please use GKMSPatch to use this module rootlessly</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>

View File

@ -1 +0,0 @@
io.github.chinosk.gakumas.localify.GakumasHookMain

View File

@ -1,3 +0,0 @@
minApiVersion=101
targetApiVersion=101
staticScope=true

View File

@ -1 +0,0 @@
com.bandainamcoent.idolmaster_gakuen

View File

@ -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"
libxposedApi = "101.0.0" xposedApi = "82"
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" }
libxposed-api = { module = "io.github.libxposed:api", version.ref = "libxposedApi" } xposed-api = { module = "de.robv.android.xposed:api", version.ref = "xposedApi" }
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" }

View File

@ -18,6 +18,7 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
maven { url "https://api.xposed.info/" }
} }
} }