Skip to content

Commit 931f97e

Browse files
committed
Add APK support to WebUIConfigDexFile and improve caching
Introduces DexSourceType enum to distinguish between DEX and APK sources for JavaScript interfaces. Refactors getInterface to support loading from both DEX files and installed APKs, uses a thread-safe cache, and improves error handling and logging.
1 parent b4fc3a4 commit 931f97e

File tree

1 file changed

+92
-37
lines changed
  • webui/src/main/kotlin/com/dergoogler/mmrl/webui/model

1 file changed

+92
-37
lines changed

webui/src/main/kotlin/com/dergoogler/mmrl/webui/model/Config.kt

Lines changed: 92 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,34 @@ package com.dergoogler.mmrl.webui.model
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.content.pm.PackageManager
56
import android.content.pm.ShortcutInfo
67
import android.content.pm.ShortcutManager
78
import android.graphics.BitmapFactory
89
import android.graphics.drawable.Icon
910
import android.util.Log
1011
import android.widget.Toast
1112
import androidx.compose.runtime.Immutable
13+
import com.dergoogler.mmrl.platform.PlatformManager
1214
import com.dergoogler.mmrl.platform.content.LocalModule
1315
import com.dergoogler.mmrl.platform.file.SuFile
16+
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
17+
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
1418
import com.dergoogler.mmrl.platform.model.ModId
1519
import com.dergoogler.mmrl.platform.model.ModId.Companion.putModId
1620
import com.dergoogler.mmrl.platform.model.ModId.Companion.webrootDir
1721
import com.dergoogler.mmrl.webui.R
1822
import com.dergoogler.mmrl.webui.activity.WXActivity
1923
import com.dergoogler.mmrl.webui.interfaces.WXInterface
2024
import com.dergoogler.mmrl.webui.moshi
21-
import com.squareup.moshi.JsonClass
2225
import com.squareup.moshi.Json
26+
import com.squareup.moshi.JsonClass
27+
import dalvik.system.BaseDexClassLoader
28+
import dalvik.system.DexClassLoader
2329
import dalvik.system.InMemoryDexClassLoader
2430
import java.nio.ByteBuffer
31+
import java.util.concurrent.ConcurrentHashMap
32+
2533

2634
object WebUIPermissions {
2735
const val PLUGIN_DEX_LOADER = "webui.permission.PLUGIN_DEX_LOADER"
@@ -81,62 +89,109 @@ data class WebUIConfigRequire(
8189
val version: WebUIConfigRequireVersion = WebUIConfigRequireVersion(),
8290
)
8391

84-
private val interfaceCache = mutableMapOf<String, JavaScriptInterface<out WXInterface>>()
92+
@JsonClass(generateAdapter = false)
93+
enum class DexSourceType {
94+
@Json(name = "dex") DEX,
95+
@Json(name = "apk") APK
96+
}
97+
98+
private val interfaceCache = ConcurrentHashMap<String, JavaScriptInterface<out WXInterface>>()
8599

86100
@JsonClass(generateAdapter = true)
87101
data class WebUIConfigDexFile(
102+
val type: DexSourceType = DexSourceType.DEX,
88103
val path: String? = null,
89104
val className: String? = null,
90105
) {
91106
private companion object {
92107
const val TAG = "WebUIConfigDexFile"
93108
}
94109

95-
fun getInterface(context: Context, modId: ModId): JavaScriptInterface<out WXInterface>? {
96-
if (className in interfaceCache) {
97-
return interfaceCache[className]
98-
}
99-
100-
if (path == null || className == null) {
101-
return null
102-
}
103-
104-
val file = SuFile(modId.webrootDir, path)
105-
106-
if (!file.isFile) {
107-
return null
108-
}
109-
110-
if (file.extension != "dex") {
111-
return null
112-
}
113-
114-
try {
115-
val dexFileParcel = file.readBytes()
116-
val loader = InMemoryDexClassLoader(
117-
ByteBuffer.wrap(dexFileParcel), context.classLoader
118-
)
119-
120-
val rawClass = loader.loadClass(className)
110+
/**
111+
* Loads and instantiates a JavaScript interface from a DEX or APK file.
112+
*
113+
* @param context The Android Context.
114+
* @param modId The ID of the mod providing the web root.
115+
* @param interfaceCache A thread-safe cache to store and retrieve loaded interfaces,
116+
* preventing redundant and expensive file operations.
117+
* @return The instantiated JavaScriptInterface, or null if loading fails.
118+
*/
119+
fun getInterface(
120+
context: Context,
121+
modId: ModId,
122+
): JavaScriptInterface<out WXInterface>? {
123+
// Use guard clauses for cleaner validation at the start.
124+
val currentClassName = className ?: return null
125+
val currentPath = path ?: return null
126+
127+
// 1. Check cache first for immediate retrieval.
128+
interfaceCache[currentClassName]?.let { return it }
129+
130+
return try {
131+
// 2. Create the appropriate class loader.
132+
val loader = when (type) {
133+
DexSourceType.DEX -> createDexLoader(context, modId, currentPath)
134+
DexSourceType.APK -> createApkLoader(context, currentPath)
135+
} ?: return null // Return null if loader creation failed.
136+
137+
// 3. Load the class and create an instance.
138+
val rawClass = loader.loadClass(currentClassName)
121139
if (!WXInterface::class.java.isAssignableFrom(rawClass)) {
122-
Log.e(TAG, "Loaded class $className does not implement WXInterface")
140+
Log.e(TAG, "Loaded class $currentClassName does not implement WXInterface")
123141
return null
124142
}
125143

126-
@Suppress("UNCHECKED_CAST") val clazz = rawClass as Class<out WXInterface>
144+
@Suppress("UNCHECKED_CAST")
145+
val clazz = rawClass as Class<out WXInterface>
127146
val instance = JavaScriptInterface(clazz)
128147

129-
interfaceCache[className] = instance
130-
return instance
148+
// 4. Cache the new instance and return it.
149+
interfaceCache.putIfAbsent(currentClassName, instance)
150+
instance
131151
} catch (e: ClassNotFoundException) {
132-
Log.e(TAG, "Class $className not found in dex file ${file.path}", e)
133-
return null
152+
Log.e(TAG, "Class $currentClassName not found in path: $currentPath", e)
153+
null
134154
} catch (e: Exception) {
135-
Log.e(
136-
TAG, "Error instantiating class $className from dex file ${file.path}", e
137-
)
155+
// Generic catch for any other instantiation or loading errors.
156+
Log.e(TAG, "Error loading class $currentClassName from path: $currentPath", e)
157+
null
158+
}
159+
}
160+
161+
/**
162+
* Creates a ClassLoader for a standalone .dex file.
163+
*/
164+
private fun createDexLoader(context: Context, modId: ModId, dexPath: String): BaseDexClassLoader? {
165+
val file = SuFile(modId.webrootDir, dexPath)
166+
167+
if (!file.isFile || file.extension != "dex") {
168+
Log.e(TAG, "Provided path is not a valid .dex file: ${file.path}")
138169
return null
139170
}
171+
172+
// Using InMemoryDexClassLoader is efficient if DEX files are not excessively large.
173+
val dexFileBytes = file.readBytes()
174+
return InMemoryDexClassLoader(ByteBuffer.wrap(dexFileBytes), context.classLoader)
175+
}
176+
177+
/**
178+
* Creates a ClassLoader for a class within an installed APK.
179+
*/
180+
private fun createApkLoader(context: Context, packageName: String): BaseDexClassLoader? {
181+
return try {
182+
val pm: HiddenPackageManager = PlatformManager.packageManager
183+
val um: HiddenUserManager = PlatformManager.userManager
184+
val appInfo = pm.getApplicationInfo(packageName, um.myUserId, 0)
185+
val apkPath = appInfo.sourceDir
186+
187+
val optimizedDir = context.getDir("dex_opt", Context.MODE_PRIVATE).absolutePath
188+
189+
DexClassLoader(apkPath, optimizedDir, null, context.classLoader)
190+
} catch (e: PackageManager.NameNotFoundException) {
191+
// Use consistent logging instead of printStackTrace.
192+
Log.e(TAG, "Could not find package: $packageName", e)
193+
null
194+
}
140195
}
141196
}
142197

0 commit comments

Comments
 (0)