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