feat: implement FireflyModMenu for automatic asset synchronization and server initialization
Build / build (push) Successful in 3m51s

This commit is contained in:
2026-06-11 14:02:23 +07:00
parent c3acbb5453
commit 4577d5f7e2
2 changed files with 223 additions and 21 deletions
@@ -6,6 +6,7 @@ import android.util.Log
import libandroid.Libandroid
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipFile
object FireflyModMenu {
@@ -14,64 +15,265 @@ object FireflyModMenu {
@JvmStatic
fun init(activity: Activity) {
val modPackageName = "com.kain344.firefly_go_android"
Log.d("FireflyMod", "Initializing FireflyModMenu for module: $modPackageName")
val appDataPath = File(activity.getExternalFilesDir(null), "FireflyGo").absolutePath
val dataDir = File("$appDataPath/data")
if (!dataDir.exists()) dataDir.mkdirs()
val sharedPrefs = activity.getSharedPreferences("FireflyModPrefs", Context.MODE_PRIVATE)
val modPackageName = "com.kain344.firefly_go_android"
val currentUpdateTime = try {
val packageInfo = activity.packageManager.getPackageInfo(modPackageName, 0)
packageInfo.lastUpdateTime
} catch (e: Exception) {
0L
val apkPath = getModuleApkPath(FireflyModMenu::class.java.classLoader)
val currentUpdateTime = if (apkPath != null) {
try {
val lastModified = File(apkPath).lastModified()
Log.d("FireflyMod", "Module APK path: $apkPath, Last modified: $lastModified")
lastModified
} catch (e: Exception) {
Log.e("FireflyMod", "Lỗi khi lấy last modified của APK: ${e.message}")
0L
}
} else {
// Fallback to PackageManager if APK path could not be resolved
try {
val packageInfo = activity.packageManager.getPackageInfo(modPackageName, 0)
Log.d("FireflyMod", "Module package found. Last update: ${packageInfo.lastUpdateTime}")
packageInfo.lastUpdateTime
} catch (e: Exception) {
Log.e("FireflyMod", "Module package $modPackageName not found: ${e.message}")
0L
}
}
val lastUpdateTime = sharedPrefs.getLong("last_update_time", 0L)
val shouldOverride = currentUpdateTime != 0L && currentUpdateTime != lastUpdateTime
if (shouldOverride) {
Log.d("FireflyMod", "Module version changed (old: $lastUpdateTime, new: $currentUpdateTime). Forcing asset override.")
}
if (!isServerStarted) {
Log.d("FireflyMod", "Start Server")
Log.d("FireflyMod", "Starting Server thread...")
isServerStarted = true
Thread {
try {
// Lấy context của module để truy cập assets đúng cách trong môi trường Xposed
val modContext = try {
activity.createPackageContext(modPackageName, Context.CONTEXT_IGNORE_SECURITY)
} catch (e: Exception) {
Log.e("FireflyMod", "Không thể tạo mod context: ${e.message}")
activity
val isCopyDone = if (apkPath != null) {
copyRawFilesFromApk(apkPath, dataDir, shouldOverride)
} else {
// Fallback to createPackageContext
val modContext = try {
activity.createPackageContext(modPackageName, Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY)
} catch (e: Exception) {
Log.e("FireflyMod", "Không thể tạo mod context cho $modPackageName: ${e.message}")
null
}
if (modContext != null) {
copyRawFiles(modContext, dataDir, shouldOverride)
} else {
false
}
}
val isCopyDone = copyRawFiles(modContext, dataDir, shouldOverride)
if (isCopyDone) {
if (shouldOverride) {
sharedPrefs.edit().putLong("last_update_time", currentUpdateTime).apply()
}
Libandroid.setPathDataLocal(appDataPath)
Libandroid.setServerRunning(true)
Log.d("FireflyMod", "Server started successfully.")
} else {
Log.e("FireflyMod", "Lỗi khi copy assets, server không được khởi chạy.")
isServerStarted = false
}
} catch (e: Exception) {
Log.e("FireflyMod", "Error starting server: ${e.message}", e)
Log.e("FireflyMod", "Error in server thread: ${e.message}", e)
isServerStarted = false
}
}.start()
}
}
private fun getModuleApkPath(classLoader: ClassLoader?): String? {
var cl = classLoader
while (cl != null) {
Log.d("FireflyMod", "Checking ClassLoader: ${cl.javaClass.name}")
// 1. Try parsing toString() first (reflection-free, safe from hidden API checks)
val pathFromToString = getModuleApkPathFromToString(cl)
if (pathFromToString != null) {
Log.d("FireflyMod", "Found APK path via ClassLoader toString(): $pathFromToString")
return pathFromToString
}
// 2. Try reflection on this class loader
val pathFromReflection = getModuleApkPathViaReflection(cl)
if (pathFromReflection != null) {
Log.d("FireflyMod", "Found APK path via reflection: $pathFromReflection")
return pathFromReflection
}
cl = cl.parent
}
Log.w("FireflyMod", "Could not find module APK path in ClassLoader hierarchy.")
return null
}
private fun getModuleApkPathFromToString(classLoader: ClassLoader): String? {
val clString = classLoader.toString()
val regexes = listOf(
Regex("""zip file "([^"]+)""""),
Regex("""zip file '([^']+)'"""),
Regex("""\[([^\]]+\.(apk|zip))\]"""),
Regex("""file:([^:\s]+\.(apk|zip))""")
)
var fallbackPath: String? = null
for (regex in regexes) {
val matches = regex.findAll(clString)
for (match in matches) {
if (match.groupValues.size > 1) {
val path = match.groupValues[1]
val file = File(path)
if (file.exists() && file.isFile) {
if (path.contains("com.kain344.firefly_go_android") && path.endsWith(".apk")) {
return path
}
if (path.endsWith(".apk") && fallbackPath == null) {
fallbackPath = path
}
}
}
}
}
// General path match for any absolute path ending with .apk or .zip
val pathRegex = Regex("""(/[^:\s"']+\.(apk|zip))""")
val pathMatches = pathRegex.findAll(clString)
for (match in pathMatches) {
val path = match.groupValues[1]
val file = File(path)
if (file.exists() && file.isFile) {
if (path.contains("com.kain344.firefly_go_android") && path.endsWith(".apk")) {
return path
}
if (path.endsWith(".apk") && fallbackPath == null) {
fallbackPath = path
}
}
}
return fallbackPath
}
private fun getModuleApkPathViaReflection(classLoader: ClassLoader): String? {
try {
val baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader")
if (!baseDexClassLoaderClass.isInstance(classLoader)) return null
val pathListField = baseDexClassLoaderClass.getDeclaredField("pathList")
pathListField.isAccessible = true
val pathList = pathListField.get(classLoader) ?: return null
val dexElementsField = pathList.javaClass.getDeclaredField("dexElements")
dexElementsField.isAccessible = true
val dexElements = dexElementsField.get(pathList) as? Array<*> ?: return null
var fallbackApkPath: String? = null
for (element in dexElements) {
if (element == null) continue
val elementClass = element.javaClass
val fileField = try {
elementClass.getDeclaredField("file")
} catch (e: NoSuchFieldException) {
try {
elementClass.getDeclaredField("path")
} catch (ex: NoSuchFieldException) {
null
}
}
if (fileField != null) {
fileField.isAccessible = true
val file = fileField.get(element) as? File
if (file != null && file.exists() && file.isFile) {
val path = file.absolutePath
if (path.contains("com.kain344.firefly_go_android") && path.endsWith(".apk")) {
return path
}
if (path.endsWith(".apk") && fallbackApkPath == null) {
fallbackApkPath = path
}
}
}
}
return fallbackApkPath
} catch (e: Exception) {
Log.d("FireflyMod", "Reflection failed on ClassLoader ${classLoader.javaClass.name}: ${e.message}")
}
return null
}
private fun copyRawFilesFromApk(apkPath: String, targetDir: File, override: Boolean = false): Boolean {
val files = listOf(
"data-in-game.json" to "data-in-game.json",
"freesr-data.json" to "freesr-data.json",
"version.json" to "version.json"
)
return try {
ZipFile(apkPath).use { zip ->
for ((assetFile, outName) in files) {
val outFile = File(targetDir, outName)
if (outFile.exists() && !override) {
Log.d("FireflyMod", "Bỏ qua $outName (đã tồn tại)")
continue
}
val entryName = "assets/$assetFile"
val entry = zip.getEntry(entryName)
if (entry == null) {
Log.e("FireflyMod", "Không tìm thấy entry $entryName trong APK")
return false
}
zip.getInputStream(entry).use { input ->
FileOutputStream(outFile).use { output ->
input.copyTo(output)
output.fd.sync()
}
}
Log.d("FireflyMod", "Đã copy $assetFile từ APK thành công (override=$override)")
}
}
true
} catch (e: Exception) {
Log.e("FireflyMod", "Lỗi khi copy file từ APK: ${e.message}", e)
false
}
}
private fun copyRawFiles(context: Context, targetDir: File, override: Boolean = false): Boolean {
val files = listOf(
"data-in-game.json" to "data-in-game.json",
"freesr-data.json" to "freesr-data.json",
"version.json" to "version.json"
)
return try {
// Kiểm tra danh sách assets có sẵn để debug
val availableAssets = context.assets.list("")?.toList() ?: emptyList()
Log.d("FireflyMod", "Assets có sẵn trong module: $availableAssets")
for ((assetFile, outName) in files) {
val outFile = File(targetDir, outName)
if (outFile.exists() && !override) continue
if (outFile.exists() && !override) {
Log.d("FireflyMod", "Bỏ qua $outName (đã tồn tại)")
continue
}
if (!availableAssets.contains(assetFile)) {
Log.w("FireflyMod", "Cảnh báo: Không tìm thấy $assetFile trong danh sách assets của module!")
}
context.assets.open(assetFile).use { input ->
FileOutputStream(outFile).use { output ->
@@ -79,12 +281,12 @@ object FireflyModMenu {
output.fd.sync()
}
}
Log.d("FireflyMod", "Copied $assetFile to ${outFile.absolutePath} (override=$override)")
Log.d("FireflyMod", "Đã copy $assetFile thành công (override=$override)")
}
true
} catch (e: Exception) {
Log.e("FireflyMod", "Error copying file: ${e.message}", e)
Log.e("FireflyMod", "Lỗi khi copy file từ assets của ${context.packageName}: ${e.message}", e)
false
}
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
{
"tag": "4.3.2-02",
"title": "PreBuild Version 4.3.52 - 02"
"tag": "4.3.2-03",
"title": "PreBuild Version 4.3.52 - 03"
}