diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
index f92cc51179..36d66fb6db 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
@@ -73,6 +73,7 @@ import com.itsaky.androidide.utils.UniqueNameBuilder
import com.itsaky.androidide.utils.flashSuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.adfa.constants.CONTENT_KEY
import org.greenrobot.eventbus.Subscribe
@@ -196,14 +197,17 @@ open class EditorHandlerActivity :
override fun onPause() {
super.onPause()
// Record timestamps for all currently open files before saving the cache
- editorViewModel.getOpenedFiles().forEach { file ->
- // Note: Using the file's absolutePath as the key
- fileTimestamps[file.absolutePath] = file.lastModified()
+ val openFiles = editorViewModel.getOpenedFiles()
+ lifecycleScope.launch(Dispatchers.IO) {
+ openFiles.forEach { file ->
+ // Note: Using the file's absolutePath as the key
+ fileTimestamps[file.absolutePath] = file.lastModified()
+ }
}
ActionContextProvider.clearActivity()
if (!isOpenedFilesSaved.get()) {
saveOpenedFiles()
- saveAllAsync(notify = false)
+ saveAllAsync(notify = false)
}
}
@@ -221,19 +225,24 @@ open class EditorHandlerActivity :
val openFiles = editorViewModel.getOpenedFiles()
if (openFiles.isEmpty() || fileTimestamps.isEmpty()) return
- // Check each open file
- openFiles.forEach { file ->
- val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: return@forEach
- val currentTimestamp = file.lastModified()
- val editorView = getEditorForFile(file)
-
- // If the file on disk is newer AND the editor for it exists AND has no unsaved changes...
- if (currentTimestamp > lastKnownTimestamp && editorView != null && !editorView.isModified) {
- val newContent = file.readText()
- editorView.editor?.post {
- editorView.editor?.setText(newContent)
- editorView.markAsSaved()
- updateTabs()
+ lifecycleScope.launch(Dispatchers.IO) {
+ // Check each open file
+ openFiles.forEach { file ->
+ val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: return@forEach
+ val currentTimestamp = file.lastModified()
+
+ // If the file on disk is newer.
+ if (currentTimestamp > lastKnownTimestamp) {
+ val newContent = runCatching { file.readText() }.getOrNull() ?: return@forEach
+ withContext(Dispatchers.Main) {
+ // If the editor for the new file exists AND has no unsaved changes...
+ val editorView = getEditorForFile(file) ?: return@withContext
+ if (editorView.isModified) return@withContext
+
+ editorView.editor?.setText(newContent)
+ editorView.markAsSaved()
+ updateTabs()
+ }
}
}
}
@@ -270,35 +279,44 @@ open class EditorHandlerActivity :
override fun onStart() {
super.onStart()
- try {
- val prefs = (application as BaseApplication).prefManager
- val jsonCache = prefs.getString(PREF_KEY_OPEN_FILES_CACHE, null)
- if (jsonCache != null) {
- val cache = Gson().fromJson(jsonCache, OpenedFilesCache::class.java)
+ lifecycleScope.launch {
+ try {
+ val prefs = (application as BaseApplication).prefManager
+ val jsonCache = withContext(Dispatchers.IO) {
+ prefs.getString(PREF_KEY_OPEN_FILES_CACHE, null)
+ } ?: return@launch
+
+ val cache = withContext(Dispatchers.Default) {
+ Gson().fromJson(jsonCache, OpenedFilesCache::class.java)
+ }
onReadOpenedFilesCache(cache)
// Clear the preference so it's only loaded once on startup
- prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null)
+ withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) }
+ } catch (err: Throwable) {
+ log.error("Failed to reopen recently opened files", err)
}
- } catch (err: Throwable) {
- log.error("Failed to reopen recently opened files", err)
}
}
private fun onReadOpenedFilesCache(cache: OpenedFilesCache?) {
cache ?: return
- val existingFiles = cache.allFiles.filter { File(it.filePath).exists() }
- val selectedFileExists = File(cache.selectedFile).exists()
+ lifecycleScope.launch(Dispatchers.IO) {
+ val existingFiles = cache.allFiles.filter { File(it.filePath).exists() }
+ val selectedFileExists = File(cache.selectedFile).exists()
- if (existingFiles.isEmpty()) return
+ if (existingFiles.isEmpty()) return@launch
- existingFiles.forEach { file ->
- openFile(File(file.filePath), file.selection)
- }
+ withContext(Dispatchers.Main) {
+ existingFiles.forEach { file ->
+ openFile(File(file.filePath), file.selection)
+ }
- if (selectedFileExists) {
- openFile(File(cache.selectedFile))
+ if (selectedFileExists) {
+ openFile(File(cache.selectedFile))
+ }
+ }
}
}
@@ -386,7 +404,11 @@ open class EditorHandlerActivity :
selection: Range?,
): CodeEditorView? {
val range = selection ?: Range.NONE
- if (ImageUtils.isImage(file)) {
+ val isImage = runBlocking {
+ withContext(Dispatchers.IO) { ImageUtils.isImage(file) }
+ }
+
+ if (isImage) {
openImage(this, file)
return null
}
@@ -515,11 +537,11 @@ open class EditorHandlerActivity :
progressConsumer: ((Int, Int) -> Unit)?,
runAfter: (() -> Unit)?,
) {
- lifecycleScope.launch {
- saveAll(notify, requestSync, processResources, progressConsumer)
- runAfter?.invoke()
- }
- }
+ lifecycleScope.launch {
+ saveAll(notify, requestSync, processResources, progressConsumer)
+ runAfter?.invoke()
+ }
+ }
override suspend fun saveAll(
notify: Boolean,
diff --git a/layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/EditorActivity.kt b/layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/EditorActivity.kt
index 7af9087946..6ca9c0dd8d 100644
--- a/layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/EditorActivity.kt
+++ b/layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/EditorActivity.kt
@@ -31,7 +31,9 @@ import com.itsaky.androidide.idetooltips.TooltipCategory
import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.utils.getCreatedTime
import com.itsaky.androidide.utils.getLastModifiedTime
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.adfa.constants.CONTENT_KEY
import org.adfa.constants.CONTENT_TITLE_KEY
import org.appdevforall.codeonthego.layouteditor.BaseActivity
@@ -100,8 +102,10 @@ class EditorActivity : BaseActivity() {
}
else -> {
- saveXml()
- finishAfterTransition()
+ lifecycleScope.launch {
+ saveXml()
+ finishAfterTransition()
+ }
}
}
}
@@ -189,37 +193,64 @@ class EditorActivity : BaseActivity() {
}
private fun androidToDesignConversion(uri: Uri?) {
- val path = uri?.path
- if (path != null && path.endsWith(".xml")) {
- val xml = FileUtil.readFromUri(uri, this@EditorActivity)
- val xmlConverted = ConvertImportedXml(xml).getXmlConverted(this@EditorActivity)
+ if (uri == null) {
+ Toast.makeText(
+ this@EditorActivity,
+ getString(string.error_invalid_xml_file),
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
+
+ lifecycleScope.launch {
+ try {
+ val fileName = FileUtil.getLastSegmentFromPath(uri.path ?: "")
+
+ if (!fileName.endsWith(".xml", ignoreCase = true)) {
+ Toast.makeText(
+ this@EditorActivity,
+ getString(string.error_invalid_xml_file),
+ Toast.LENGTH_SHORT
+ ).show()
+ return@launch
+ }
+
+ val xml = withContext(Dispatchers.IO) {
+ FileUtil.readFromUri(uri, this@EditorActivity)
+ } ?: run {
+ make(binding.root, getString(string.error_failed_to_import))
+ .setSlideAnimation()
+ .showAsError()
+ return@launch
+ }
- if (xmlConverted != null) {
- val fileName = FileUtil.getLastSegmentFromPath(path)
+ val xmlConverted = withContext(Dispatchers.Default) {
+ ConvertImportedXml(xml).getXmlConverted(this@EditorActivity)
+ }
+
+ if (xmlConverted == null) {
+ make(binding.root, getString(string.error_failed_to_import))
+ .setSlideAnimation()
+ .showAsError()
+ return@launch
+ }
val productionPath = project.layoutPath + fileName
val designPath = project.layoutDesignPath + fileName
-
- FileUtil.writeFile(productionPath, xml)
- FileUtil.writeFile(designPath, xmlConverted)
+ withContext(Dispatchers.IO) {
+ FileUtil.writeFile(productionPath, xml)
+ FileUtil.writeFile(designPath, xmlConverted)
+ }
openLayout(LayoutFile(productionPath, designPath))
-
make(binding.root, getString(string.success_imported))
.setFadeAnimation()
.showAsSuccess()
- } else {
+ } catch (t: Throwable) {
make(binding.root, getString(string.error_failed_to_import))
.setSlideAnimation()
.showAsError()
}
- } else {
- Toast
- .makeText(
- this@EditorActivity,
- getString(string.error_invalid_xml_file),
- Toast.LENGTH_SHORT,
- ).show()
}
}
@@ -426,11 +457,13 @@ class EditorActivity : BaseActivity() {
if (result.isEmpty()) {
showNothingDialog()
} else {
- saveXml()
- startActivity(
- Intent(this, PreviewLayoutActivity::class.java)
- .putExtra(Constants.EXTRA_KEY_LAYOUT, project.currentLayout),
- )
+ lifecycleScope.launch {
+ saveXml()
+ startActivity(
+ Intent(this@EditorActivity, PreviewLayoutActivity::class.java)
+ .putExtra(Constants.EXTRA_KEY_LAYOUT, project.currentLayout),
+ )
+ }
}
return true
}
@@ -522,8 +555,10 @@ class EditorActivity : BaseActivity() {
if (binding.editorLayout.isLayoutModified()) {
showSaveChangesDialog()
} else {
- saveXml()
- finishAfterTransition()
+ lifecycleScope.launch {
+ saveXml()
+ finishAfterTransition()
+ }
}
return true
}
@@ -563,8 +598,10 @@ class EditorActivity : BaseActivity() {
if (result.isEmpty()) {
showNothingDialog()
} else {
- saveXml()
- finish()
+ lifecycleScope.launch {
+ saveXml()
+ finish()
+ }
}
}
@@ -686,23 +723,53 @@ class EditorActivity : BaseActivity() {
.show()
}
- private fun saveXml() {
- val currentLayoutFile = project.currentLayout as? LayoutFile ?: return
+ private fun currentLayoutFileOrNull(): LayoutFile? =
+ project.currentLayout as? LayoutFile
- if (binding.editorLayout.isEmpty()) {
- currentLayoutFile.saveLayout("")
- currentLayoutFile.saveDesignFile("")
- binding.editorLayout.markAsSaved()
- ToastUtils.showShort(getString(string.layout_saved))
- return
+ private fun restoreOriginalXmlIfNeeded() {
+ val xmlToRestore = originalDesignXml ?: originalProductionXml
+ if (!xmlToRestore.isNullOrBlank()) {
+ binding.editorLayout.loadLayoutFromParser(xmlToRestore)
}
+ }
+
+ /**
+ * Writes the current editor state to disk.
+ * - Generates XML on the current thread (UI-safe)
+ * - Performs file I/O on Dispatchers.IO
+ * - No UI side-effects (no toast / no markAsSaved) to keep it reusable
+ */
+ private suspend fun persistEditorLayout(layoutFile: LayoutFile): Boolean {
+ return runCatching {
+ if (binding.editorLayout.isEmpty()) {
+ withContext(Dispatchers.IO) {
+ layoutFile.saveLayout("")
+ layoutFile.saveDesignFile("")
+ }
+ return@runCatching
+ }
- val productionXml = XmlLayoutGenerator().generate(binding.editorLayout, true)
- currentLayoutFile.saveLayout(productionXml)
+ val generator = XmlLayoutGenerator()
+ val productionXml = generator.generate(binding.editorLayout, true)
+ val designXml = generator.generate(binding.editorLayout, false)
- // Generate and save the DESIGN-TIME XML for the editor's internal use
- val designXml = XmlLayoutGenerator().generate(binding.editorLayout, false)
- currentLayoutFile.saveDesignFile(designXml)
+ withContext(Dispatchers.IO) {
+ layoutFile.saveLayout(productionXml)
+ layoutFile.saveDesignFile(designXml)
+ }
+ }.isSuccess
+ }
+
+ private suspend fun saveXml() {
+ val layoutFile = currentLayoutFileOrNull() ?: return
+
+ val success = persistEditorLayout(layoutFile)
+ if (!success) {
+ withContext(Dispatchers.Main) {
+ ToastUtils.showShort(getString(string.failed_to_save_layout))
+ }
+ return
+ }
binding.editorLayout.markAsSaved()
ToastUtils.showShort(getString(string.layout_saved))
@@ -713,25 +780,20 @@ class EditorActivity : BaseActivity() {
.setTitle(R.string.save_changes)
.setMessage(R.string.msg_save_changes_to_layout)
.setPositiveButton(R.string.save_changes_and_exit) { _, _ ->
- saveXml()
- finishAfterTransition()
+ lifecycleScope.launch {
+ saveXml()
+ finishAfterTransition()
+ }
}.setNegativeButton(R.string.discard_changes_and_exit) { _, _ ->
- val layoutFile = project.currentLayout as? LayoutFile ?: run {
- finishAfterTransition()
- return@setNegativeButton
- }
-
- val xmlToRestore = originalDesignXml ?: originalProductionXml
- if (!xmlToRestore.isNullOrBlank()) {
- binding.editorLayout.loadLayoutFromParser(xmlToRestore)
+ lifecycleScope.launch {
+ val layoutFile = currentLayoutFileOrNull() ?: run {
+ finishAfterTransition()
+ return@launch
+ }
+ restoreOriginalXmlIfNeeded()
+ persistEditorLayout(layoutFile)
+ finishAfterTransition()
}
-
- val prettyXml = XmlLayoutGenerator().generate(binding.editorLayout, true)
- layoutFile.saveLayout(prettyXml)
-
- val designXml = XmlLayoutGenerator().generate(binding.editorLayout, false)
- layoutFile.saveDesignFile(designXml)
- finishAfterTransition()
}.setNeutralButton(R.string.cancel_and_stay_in_editor) { dialog, _ ->
dialog.dismiss()
}.setCancelable(false)
diff --git a/layouteditor/src/main/res/values/strings.xml b/layouteditor/src/main/res/values/strings.xml
index a664a55b93..a4d1488e9c 100644
--- a/layouteditor/src/main/res/values/strings.xml
+++ b/layouteditor/src/main/res/values/strings.xml
@@ -7,6 +7,7 @@
Project saved.
New project
Layout Saved
+ Failed to save layout
New Layout
Layout empty! Add a view before saving…
Add new