Skip to content

Commit ebff162

Browse files
ADFA-6016: Fix Layout Editor crash on release IDE builds (#98)
* Fix Layout Editor crash on release IDE builds LayoutEditorFragment crashed at onCreate with ClassCastException: LayoutEditorFragment cannot be cast to androidx.activity.result.ActivityResultCaller. The release IDE APK is R8-shrunk and nothing in it uses ActivityResultCaller as a type, so R8 drops the interface and strips it from Fragment's implements list. The plugin bundles androidx.activity, and the plugin DexClassLoader is parent-first: Fragment resolves to the host's class while ActivityResultCaller falls through to the plugin's own bundled copy. The two are unrelated types, so passing the fragment into FileCreator/FilePicker (both declared ActivityResultCaller) failed the cast. Unminified debug IDE builds keep the interface, which is why this only showed up on release. Take Fragment instead and call registerForActivityResult on it, matching APK-Analyzer, Markdown-Previewer and Sketch-to-UI. ActivityResultContract, ActivityResultCallback and ActivityResultLauncher all survive in the release host, so the launchers still cross the boundary correctly. The unused AppCompatActivity constructor of FilePicker goes with it. Verified on an emulator running the release IDE (C-r-0911-2020): plugin installs, Layout Editor renders activity_main.xml, and Resources Manager (three FilePicker instances) opens without a crash. * Stop Layout Editor calling Apache Commons methods the host shrank away Same root cause as the ActivityResultCaller fix: the plugin DexClassLoader is parent-first, so any class the R8-shrunk release IDE also ships resolves to the host's copy. commons-text and commons-io are both in the host, but only the slice the IDE itself uses survived R8: StringEscapeUtils keeps the ESCAPE_* fields and unescapeJava; every escapeXxx(String) method is gone, escapeXml11 included FileUtils keeps listFiles(File, IOFileFilter, IOFileFilter) and listFiles(File, FileFilter); the (File, String[], boolean) overload the plugin calls is gone escapeXml11 is the NoSuchMethodError reported from XmlLayoutGenerator; the listFiles one had not been hit yet but crashes Resources Manager the moment a project has a drawable folder. Neither needs a library. TextUtils.htmlEncode escapes the same characters an XML attribute or element body needs and is a framework API, so no amount of host shrinking can remove it. File.listFiles with a filter is what the same file already does a few lines down in buildDpiVersionMap. With those two gone nothing else in the plugin uses Apache Commons, so both dependencies go too (9.2 MB -> 8.7 MB). StringResourceAdapter used lang3's deprecated StringEscapeUtils, which happens to work today only because the host dropped that class entirely and the plugin's bundled copy loads instead. Same treatment, so it cannot regress when the host's keep rules change. Verified on an emulator running the release IDE (C-r-0911-2020): the layout renders, navigating back to the editor no longer crashes, Export Layout runs the generator end to end, and Resources Manager lists a drawable folder (png/jpg/gif/xml, correctly skipping webp). * ADFA-6016 | Fix the Compose Preview compiler toolchain against Kotlin 2.3.21 Two defects sat in front of the preview, each hidden by the one before it. Both show up only on a release IDE, because that is where the plugin gets far enough to start the compiler at all. The Kotlin compiler runs as a JVM subprocess, and its bootstrap classpath listed six artifacts with no kotlinx-coroutines. kotlin-compiler-embeddable 2.3.21 touches CoroutineScope while building its CoreApplicationEnvironment, so the subprocess died with NoClassDefFoundError before compiling anything. The jar is already in the IDE's bundled local Maven repo; it was simply never referenced. Past that, the compiler rejected the bundled Compose compiler plugin: the compose-jars.zip asset carries 1.5.10, which demands Kotlin 1.9.22. Since Kotlin 2.0 the Compose compiler ships with Kotlin, and the same bundled repo holds kotlin-compose-compiler-plugin-embeddable 2.3.21, so resolve it there and stay version-matched to whichever compiler the IDE ships rather than to a zip frozen in June. getRuntimeJars keeps excluding the bundled jar by path so the stale copy cannot leak onto the runtime classpath. Verified on an emulator running a release IDE built from this branch's proguard change: Compose Preview renders a @Preview composable on-device. --------- Co-authored-by: Daniel Alome <astrocoder007@gmail.com>
1 parent a1e6f36 commit ebff162

7 files changed

Lines changed: 27 additions & 50 deletions

File tree

‎plugins/Jetpack-Compose-Preview/src/main/kotlin/org/appdevforall/composepreview/compiler/ComposeClasspathManager.kt‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class ComposeClasspathManager(private val context: Context) {
4141
"kotlin-reflect" to "org/jetbrains/kotlin/kotlin-reflect",
4242
"kotlin-script-runtime" to "org/jetbrains/kotlin/kotlin-script-runtime",
4343
"trove4j" to "org/jetbrains/intellij/deps/trove4j",
44+
"kotlinx-coroutines" to "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm",
45+
"compose-compiler-plugin" to "org/jetbrains/kotlin/kotlin-compose-compiler-plugin-embeddable",
4446
"annotations" to "org/jetbrains/annotations"
4547
)
4648

@@ -160,7 +162,8 @@ class ComposeClasspathManager(private val context: Context) {
160162
}
161163

162164
fun getCompilerPlugin(): File {
163-
return File(composeDir, "compose-compiler-plugin.jar")
165+
return findMavenJar("compose-compiler-plugin")
166+
?: error("kotlin-compose-compiler-plugin-embeddable not found in $localMavenRepo")
164167
}
165168

166169
fun getKotlinStdlib(): File? {
@@ -174,16 +177,17 @@ class ComposeClasspathManager(private val context: Context) {
174177
findMavenJar("kotlin-reflect")?.let { add(it) }
175178
findMavenJar("kotlin-script-runtime")?.let { add(it) }
176179
findMavenJar("trove4j")?.let { add(it) }
180+
findMavenJar("kotlinx-coroutines")?.let { add(it) }
177181
findMavenJar("annotations")?.let { add(it) }
178182
}
179183
return jars.filter { it.exists() }
180184
.joinToString(File.pathSeparator) { it.absolutePath }
181185
}
182186

183187
fun getRuntimeJars(): List<File> {
184-
val compilerPlugin = getCompilerPlugin()
188+
val bundledPlugin = File(composeDir, "compose-compiler-plugin.jar")
185189
return composeDir.listFiles { file ->
186-
file.extension == "jar" && file != compilerPlugin
190+
file.extension == "jar" && file != bundledPlugin
187191
}?.toList() ?: emptyList()
188192
}
189193

‎plugins/Layout-Editor/build.gradle.kts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ dependencies {
8989
implementation(platform("io.github.Rosemoe.sora-editor:bom:0.23.6"))
9090
implementation("io.github.Rosemoe.sora-editor:editor")
9191
implementation("io.github.Rosemoe.sora-editor:language-textmate")
92-
implementation("org.apache.commons:commons-text:1.11.0")
93-
implementation("commons-io:commons-io:2.15.1")
9492
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0")
9593
}
9694

‎plugins/Layout-Editor/src/main/java/org/appdevforall/codeonthego/layouteditor/adapters/StringResourceAdapter.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.appdevforall.codeonthego.layouteditor.adapters
33
import org.appdevforall.codeonthego.layouteditor.pluginDialogContext
44
import android.content.DialogInterface
55
import android.text.Editable
6+
import android.text.TextUtils
67
import android.text.TextWatcher
78
import android.view.LayoutInflater
89
import android.view.MenuItem
@@ -14,7 +15,6 @@ import androidx.appcompat.widget.PopupMenu
1415
import androidx.recyclerview.widget.RecyclerView
1516
import com.blankj.utilcode.util.ClipboardUtils
1617
import com.google.android.material.dialog.MaterialAlertDialogBuilder
17-
import org.apache.commons.lang3.StringEscapeUtils
1818
import org.appdevforall.codeonthego.layouteditor.ProjectFile
1919
import org.appdevforall.codeonthego.layouteditor.R
2020
import org.appdevforall.codeonthego.layouteditor.adapters.models.ValuesItem
@@ -67,7 +67,7 @@ class StringResourceAdapter(
6767
sb.append("\t<string name=\"")
6868
.append(name)
6969
.append("\">")
70-
.append(StringEscapeUtils.escapeXml11(value))
70+
.append(TextUtils.htmlEncode(value))
7171
.append("</string>\n")
7272
}
7373
sb.append("</resources>")

‎plugins/Layout-Editor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/DrawableFragment.kt‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import org.appdevforall.codeonthego.layouteditor.showPluginToast
2727
import kotlinx.coroutines.Dispatchers
2828
import kotlinx.coroutines.launch
2929
import kotlinx.coroutines.withContext
30-
import org.apache.commons.io.FileUtils
3130
import org.appdevforall.codeonthego.layouteditor.ProjectFile
3231
import org.appdevforall.codeonthego.layouteditor.R
3332
import org.appdevforall.codeonthego.layouteditor.adapters.DPIsListAdapter
@@ -58,6 +57,7 @@ class DrawableFragment : Fragment() {
5857
private var adapter: DrawableResourceAdapter? = null
5958
var dpiAdapter: DPIsListAdapter? = null
6059
private var dpiList = mutableListOf("ldpi", "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi")
60+
private val drawableExtensions = setOf("png", "jpg", "jpeg", "gif", "xml")
6161

6262
private val logger = LoggerFactory.getLogger(DrawableFragment::class.java)
6363

@@ -138,11 +138,9 @@ class DrawableFragment : Fragment() {
138138

139139
val dpiVersionMap = buildDpiVersionMap(projectDir)
140140

141-
val drawableFiles = FileUtils.listFiles(
142-
baseDrawableFolder,
143-
arrayOf("png", "jpg", "jpeg", "gif", "xml"),
144-
false
145-
)
141+
val drawableFiles = baseDrawableFolder.listFiles { file ->
142+
file.isFile && file.extension in drawableExtensions
143+
}.orEmpty()
146144

147145
drawableFiles.mapNotNull { file ->
148146
createDrawableFile(context, file, dpiVersionMap)

‎plugins/Layout-Editor/src/main/java/org/appdevforall/codeonthego/layouteditor/tools/XmlLayoutGenerator.java‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.appdevforall.codeonthego.layouteditor.tools;
22

3+
import android.text.TextUtils;
34
import android.view.View;
45
import android.view.ViewGroup;
56
import android.widget.CalendarView;
@@ -11,7 +12,6 @@
1112
import com.google.android.material.navigation.NavigationView;
1213
import com.google.android.material.tabs.TabLayout;
1314

14-
import org.apache.commons.text.StringEscapeUtils;
1515
import org.appdevforall.codeonthego.layouteditor.editor.DesignEditor;
1616
import org.appdevforall.codeonthego.layouteditor.editor.initializer.AttributeMap;
1717

@@ -68,7 +68,7 @@ private void peek(View view, HashMap<View, AttributeMap> attributeMap, int depth
6868
List<String> keys =
6969
(attributeMap.get(view) != null) ? new ArrayList<>(Objects.requireNonNull(attributeMap.get(view)).keySet()) : new ArrayList<>();
7070
for (String key : keys) {
71-
builder.append(TAB).append(indent).append(key).append("=\"").append(StringEscapeUtils.escapeXml11(Objects.requireNonNull(attributeMap.get(view)).getValue(key))).append("\"\n");
71+
builder.append(TAB).append(indent).append(key).append("=\"").append(TextUtils.htmlEncode(Objects.requireNonNull(attributeMap.get(view)).getValue(key))).append("\"\n");
7272
}
7373

7474
if (builder.charAt(builder.length() - 1) == '\n') {
@@ -131,7 +131,7 @@ private boolean tryWriteInclude(View view, HashMap<View, AttributeMap> attribute
131131

132132
builder.append("\n").append(indent).append(TAB)
133133
.append(key).append("=\"")
134-
.append(StringEscapeUtils.escapeXml11(attrs.getValue(key)))
134+
.append(TextUtils.htmlEncode(attrs.getValue(key)))
135135
.append("\"");
136136
}
137137
builder.append(" />\n\n");
@@ -152,7 +152,7 @@ private boolean tryWriteFragment(View view, HashMap<View, AttributeMap> attribut
152152

153153
builder.append("\n").append(indent).append(TAB)
154154
.append(key).append("=\"")
155-
.append(StringEscapeUtils.escapeXml11(attrs.getValue(key)))
155+
.append(TextUtils.htmlEncode(attrs.getValue(key)))
156156
.append("\"");
157157
}
158158
builder.append(" />\n\n");
@@ -173,7 +173,7 @@ private boolean tryWriteMerge(View view, HashMap<View, AttributeMap> attributeMa
173173

174174
builder.append("\n").append(indent).append(TAB)
175175
.append(key).append("=\"")
176-
.append(StringEscapeUtils.escapeXml11(attrs.getValue(key)))
176+
.append(TextUtils.htmlEncode(attrs.getValue(key)))
177177
.append("\"");
178178
}
179179

‎plugins/Layout-Editor/src/main/java/org/appdevforall/codeonthego/layouteditor/utils/FileCreator.kt‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
11
package org.appdevforall.codeonthego.layouteditor.utils
22

33
import android.net.Uri
4-
import androidx.activity.result.ActivityResultCaller
54
import androidx.activity.result.ActivityResultLauncher
65
import androidx.activity.result.contract.ActivityResultContracts
6+
import androidx.fragment.app.Fragment
77

88
/**
99
* FileCreator Class is used to create a file with given name and MIME Type.
1010
*/
11-
abstract class FileCreator(caller: ActivityResultCaller) {
11+
abstract class FileCreator(fragment: Fragment) {
1212
/** To create a file */
1313
private val createFile: ActivityResultLauncher<String>
1414

1515
/** MIME Type of file */
1616
private var mimeType = "*/*"
1717

18-
/**
19-
* Constructor of class
20-
*
21-
* @param actvty Instance of AppCompatActivity
22-
*/
2318
init {
24-
// Set MIME type
25-
// Register activity result for CreateDocument
2619
this.createFile =
27-
caller.registerForActivityResult<String, Uri>(
20+
fragment.registerForActivityResult<String, Uri>(
2821
ActivityResultContracts.CreateDocument(mimeType)
2922
) { onCreateFile(it) }
3023
}

‎plugins/Layout-Editor/src/main/java/org/appdevforall/codeonthego/layouteditor/utils/FilePicker.kt‎

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,29 @@
11
package org.appdevforall.codeonthego.layouteditor.utils
22

33
import android.Manifest
4-
import android.app.Activity
5-
import android.content.Context
64
import android.content.pm.PackageManager
75
import android.net.Uri
86
import android.os.Build
97
import android.view.View
10-
import androidx.activity.result.ActivityResultCaller
118
import androidx.activity.result.ActivityResultLauncher
129
import androidx.activity.result.contract.ActivityResultContracts
13-
import androidx.appcompat.app.AppCompatActivity
1410
import androidx.core.content.ContextCompat
1511
import androidx.fragment.app.Fragment
1612
import org.appdevforall.codeonthego.layouteditor.R
1713
import org.appdevforall.codeonthego.layouteditor.utils.SBUtils.Companion.make
1814

1915
/**
20-
* Class for FilePicker. Works from either a Fragment (plugin editor) or an AppCompatActivity
21-
* (legacy sub-activities), registering its result launchers on the given caller.
16+
* Class for FilePicker. Registers its result launchers on the hosting fragment.
2217
*/
23-
abstract class FilePicker private constructor(
24-
caller: ActivityResultCaller,
25-
private val activityProvider: () -> Activity,
26-
private val contextProvider: () -> Context,
27-
) {
28-
constructor(fragment: Fragment) : this(
29-
fragment,
30-
{ fragment.requireActivity() },
31-
{ fragment.requireContext() },
32-
)
33-
34-
constructor(activity: AppCompatActivity) : this(activity, { activity }, { activity })
18+
abstract class FilePicker(private val fragment: Fragment) {
3519

3620
private val getFile: ActivityResultLauncher<String> =
37-
caller.registerForActivityResult(ActivityResultContracts.GetContent()) { onPickFile(it) }
21+
fragment.registerForActivityResult(ActivityResultContracts.GetContent()) { onPickFile(it) }
3822

3923
private val reqPermission: ActivityResultLauncher<String> =
40-
caller.registerForActivityResult(ActivityResultContracts.RequestPermission()) { onRequestPermission(it) }
24+
fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) { onRequestPermission(it) }
4125

42-
private fun contentRoot(): View = activityProvider().findViewById(android.R.id.content)
26+
private fun contentRoot(): View = fragment.requireActivity().findViewById(android.R.id.content)
4327

4428
private fun onRequestPermission(isGranted: Boolean) {
4529
if (isGranted) make(contentRoot(), R.string.permission_granted)
@@ -70,7 +54,7 @@ abstract class FilePicker private constructor(
7054
mimeType == "image/*" || mimeType == "image/png" || mimeType == "image/jpg" || mimeType == "image/jpeg"
7155

7256
if (isImageType) {
73-
val ctx = contextProvider()
57+
val ctx = fragment.requireContext()
7458
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
7559
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_MEDIA_IMAGES)
7660
== PackageManager.PERMISSION_DENIED

0 commit comments

Comments
 (0)