Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dde4bf2
ADFA-4649: Replace SizeUtils/ConvertUtils dp-px conversions with nati…
davidschachterADFA Jul 24, 2026
a31227c
ADFA-4649: Replace ThreadUtils with native Handler-based main-thread …
davidschachterADFA Jul 24, 2026
bf80219
ADFA-4649: Replace FileUtils/FileIOUtils with native java.io equivalents
davidschachterADFA Jul 24, 2026
2f9e1e3
ADFA-4649: Replace ClipboardUtils/KeyboardUtils/NetworkUtils with nat…
davidschachterADFA Jul 24, 2026
29c438b
ADFA-4649: Replace remaining misc blankj utilities with native equiva…
davidschachterADFA Jul 24, 2026
d96cadd
ADFA-4649: Replace ReflectUtils/ImageUtils/ResourceUtils/ZipUtils wit…
davidschachterADFA Jul 24, 2026
200e074
ADFA-4649: Remove com.blankj:utilcodex dependency
davidschachterADFA Jul 24, 2026
da37d47
ADFA-4649: Document why BaseApplication has its own foregroundActivit…
davidschachterADFA Jul 24, 2026
d0d1476
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 24, 2026
1baf30d
docs: retro for LeakCanary/JAXP/PDF.js/blankj-utilcodex session
davidschachterADFA Jul 24, 2026
9d99c61
Merge remote-tracking branch 'origin/fix/ADFA-4649-remove-blankj-util…
davidschachterADFA Jul 24, 2026
90ea7f4
ADFA-4649: Fix two regressions in FileUtils blankj replacement
davidschachterADFA Jul 27, 2026
5be744e
ADFA-4649: Address code review feedback on blankj replacement
davidschachterADFA Jul 27, 2026
75bc16f
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 27, 2026
fd02074
ADFA-4649: Fix two more code review findings
davidschachterADFA Jul 27, 2026
c548b56
Merge branch 'fix/ADFA-4649-remove-blankj-utilcode' of github.com:app…
davidschachterADFA Jul 27, 2026
4c1178b
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 28, 2026
f83d363
ADFA-4649: Fix unsafe !! in CopyPathAction
davidschachterADFA Jul 28, 2026
b8f6325
Merge remote-tracking branch 'origin/stage' into fix/ADFA-4649-remove…
davidschachterADFA Jul 28, 2026
86efc75
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 28, 2026
834bc55
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 28, 2026
43b9d4a
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 28, 2026
ae09065
ADFA-4649: Address remaining code review findings on blankj replacement
davidschachterADFA Jul 29, 2026
362d7d6
Merge branch 'stage' into fix/ADFA-4649-remove-blankj-utilcode
davidschachterADFA Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ flox activate -d flox/local -- ./gradlew <task>
- **Debug APK (arm64, the usual target):** `flox activate -d flox/local -- ./gradlew :app:assembleV8Debug --parallel --max-workers=6`
- **Single unit test:** `flox activate -d flox/local -- ./gradlew :module:test --tests "com.itsaky.androidide.SomeTest"`
- **Module unit tests:** `flox activate -d flox/local -- ./gradlew :testing:unit:test`
- **Fast iteration:** during multi-file/multi-module changes, verify with targeted `:module:compileV8DebugKotlin`/`compileV8DebugJavaWithJavac` invocations (batch several modules into one Gradle call) rather than a full assemble. Reserve `:app:assembleV8Debug` for final end-to-end verification — it's slow (multi-minute) in this multi-module project, and running it after every small change adds up.

When the user names a CI/CD job ("the sonar job", "the analyze workflow"), read `.github/workflows/*.yml` — the YAML is the authoritative gradle/shell invocation. Don't reverse-engineer it from gradle tasks or build files.

Expand All @@ -37,7 +38,7 @@ See **[ARCHITECTURE.md](ARCHITECTURE.md)** — the single source of truth for th
- **Avoid new dependencies** — the build almost certainly already has what's needed. Check `gradle/libs.versions.toml` and `build.gradle.kts` first.
- **Persistence:** prefer **Room** for relational data and the filesystem/preferences for settings; raw SQLite only for justified exceptions — see [ADR 0001](docs/adr/0001-prefer-room-for-persistence.md).
- **Protect the two Android system bars** in any UI work: the top status bar (clock, notifications, status icons) and the bottom navigation bar (home, back, recents). Don't draw over or intercept them.
- **Plan and size before building.** Prefer **one PR per ticket/use case** — don't force-split a coherent change (splitting has its own overhead when later edits span the pieces). When a change is large, break it into **reviewable commits** — mechanical/refactor commits separate from behavioral ones — and offer review-by-commit. Treat ~500 LOC / ~10 files as a signal to reach for that commit structure, not a hard cap; the ceiling rises as LLM-assisted review matures.
- **Plan and size before building.** Prefer **one PR per ticket/use case** — don't force-split a coherent change (splitting has its own overhead when later edits span the pieces). When a change is large, break it into **reviewable commits** — mechanical/refactor commits separate from behavioral ones — and offer review-by-commit. Treat ~500 LOC / ~10 files as a signal to reach for that commit structure, not a hard cap; the ceiling rises as LLM-assisted review matures. For staged multi-commit refactors (e.g. removing a dependency across many files/modules), order stages easiest-to-hardest and independently compile/test each stage (see Build & test's fast-iteration guidance) before moving to the next, so a failure is isolated to the stage that caused it.
- **Keep docs in step with code.** When you change code, update the docs that describe it in the same change — a module's `README.md`, `ARCHITECTURE.md`, or an ADR — so a doc never outlives the API it documents (see REVIEW.md, Code quality). If the doc fix is out of scope, file a ticket rather than let it drift.
- `.androidide_root` is a sentinel file tests use to locate the project root — don't delete it.
- Avoid http or https links which go off-device. When such links are unavoidable, warn the user beforehand and offer to cancel the action.
Expand Down
1 change: 0 additions & 1 deletion actions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ dependencies {
implementation(libs.common.editor)
implementation(libs.common.kotlin)
implementation(libs.common.kotlin.coroutines.android)
implementation(libs.common.utilcode)
implementation(libs.google.auto.service.annotations)

implementation(libs.androidx.core.ktx)
Expand Down
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ dependencies {

implementation(platform(libs.sora.bom))
implementation(libs.common.editor)
implementation(libs.common.utilcode)
implementation(libs.common.glide)
implementation(libs.common.jsoup)
implementation(libs.common.kotlin.coroutines.android)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.itsaky.androidide.actions


import com.blankj.utilcode.util.FileIOUtils
import com.itsaky.androidide.actions.observers.FileActionObserver
import com.itsaky.androidide.eventbus.events.file.FileCreationEvent
import com.itsaky.androidide.utils.FileIOUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -13,33 +13,42 @@ import org.greenrobot.eventbus.EventBus
import java.io.File

class FileActionManager {
private val scope = CoroutineScope(Dispatchers.IO)
private val scope = CoroutineScope(Dispatchers.IO)

fun createFile(baseDir: File, path: String, content: String, observer: FileActionObserver? = null) {
scope.launch {
val result = try {
val targetFile = File(baseDir, path)
fun createFile(
baseDir: File,
path: String,
content: String,
observer: FileActionObserver? = null,
) {
scope.launch {
val result =
try {
val targetFile = File(baseDir, path)

val formattedContent = StringEscapeUtils.unescapeJava(content)
if (!FileIOUtils.writeFileFromString(targetFile, formattedContent)) {
Result.failure(Exception("Failed to write to file at path: $path"))
} else {
EventBus.getDefault().post(FileCreationEvent(targetFile))
Result.success(targetFile)
}
} catch (e: Exception) {
Result.failure(e)
}
val formattedContent = StringEscapeUtils.unescapeJava(content)
if (!FileIOUtils.writeFileFromString(targetFile, formattedContent)) {
Result.failure(Exception("Failed to write to file at path: $path"))
} else {
EventBus.getDefault().post(FileCreationEvent(targetFile))
Result.success(targetFile)
}
} catch (e: CancellationException) {
throw e
} catch (e: IllegalArgumentException) {
// StringEscapeUtils.unescapeJava rejects malformed escape sequences this way.
Result.failure(e)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

withContext(Dispatchers.Main) {
if (result.isSuccess) {
val file = result.getOrNull()
observer?.onActionSuccess("File '${file?.name}' created successfully.", file)
} else {
val error = result.exceptionOrNull()?.message ?: "An unknown error occurred."
observer?.onActionFailure("Error: $error")
}
}
}
}
withContext(Dispatchers.Main) {
if (result.isSuccess) {
val file = result.getOrNull()
observer?.onActionSuccess("File '${file?.name}' created successfully.", file)
} else {
val error = result.exceptionOrNull()?.message ?: "An unknown error occurred."
observer?.onActionFailure("Error: $error")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,30 @@
package com.itsaky.androidide.actions.filetree

import android.content.Context
import com.blankj.utilcode.util.ClipboardUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.copyToClipboard
import com.itsaky.androidide.utils.flashSuccess

/**
* Action to copy the absolute path of the selected file.
*
* @author Akash Yadav
*/
class CopyPathAction(context: Context, override val order: Int) :
BaseFileTreeAction(context, labelRes = R.string.copy_path, iconRes = R.drawable.ic_copy) {
class CopyPathAction(
context: Context,
override val order: Int,
) : BaseFileTreeAction(context, labelRes = R.string.copy_path, iconRes = R.drawable.ic_copy) {
override val id: String = "ide.editor.fileTree.copyPath"

override val id: String = "ide.editor.fileTree.copyPath"
override fun retrieveTooltipTag(isAlternateContext: Boolean): String =
TooltipTag.PROJECT_ITEM_COPYPATH
override fun retrieveTooltipTag(isAlternateContext: Boolean): String = TooltipTag.PROJECT_ITEM_COPYPATH

override suspend fun execAction(data: ActionData) {
val file = data.requireFile()
ClipboardUtils.copyText("[AndroidIDE] Copied File Path", file.absolutePath)
flashSuccess(R.string.copied)
}
override suspend fun execAction(data: ActionData) {
val file = data.requireFile()
data.requireContext().copyToClipboard(file.absolutePath, label = "[AndroidIDE] Copied File Path")
flashSuccess(R.string.copied)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.itsaky.androidide.actions.filetree

import android.app.ProgressDialog
import android.content.Context
import com.blankj.utilcode.util.FileUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent
Expand All @@ -28,6 +27,7 @@ import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.FileUtils
import com.itsaky.androidide.utils.FlashType
import com.itsaky.androidide.utils.flashMessage
import com.itsaky.androidide.utils.showWithLongPressTooltip
Expand All @@ -39,74 +39,77 @@ import java.io.File
*
* @author Akash Yadav
*/
class DeleteAction(context: Context, override val order: Int) :
BaseFileTreeAction(context, labelRes = R.string.delete_file, iconRes = R.drawable.ic_delete) {
class DeleteAction(
context: Context,
override val order: Int,
) : BaseFileTreeAction(context, labelRes = R.string.delete_file, iconRes = R.drawable.ic_delete) {
override val id: String = "ide.editor.fileTree.delete"

override val id: String = "ide.editor.fileTree.delete"
override fun retrieveTooltipTag(isAlternateContext: Boolean): String =
TooltipTag.PROJECT_ITEM_DELETE
override fun retrieveTooltipTag(isAlternateContext: Boolean): String = TooltipTag.PROJECT_ITEM_DELETE

override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
DialogUtils.newMaterialDialogBuilder(context)
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { dialogInterface, _ ->
dialogInterface.dismiss()
@Suppress("DEPRECATION")
val progressDialog =
ProgressDialog.show(context, null, context.getString(R.string.please_wait), true, false)
executeAsync({ FileUtils.delete(file) }) {
progressDialog.dismiss()
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
DialogUtils
.newMaterialDialogBuilder(context)
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { dialogInterface, _ ->
dialogInterface.dismiss()
@Suppress("DEPRECATION")
val progressDialog =
ProgressDialog.show(context, null, context.getString(R.string.please_wait), true, false)
executeAsync({ FileUtils.delete(file) }) {
progressDialog.dismiss()

val deleted = it ?: false
val deleted = it ?: false

flashMessage(
if (deleted) R.string.deleted else R.string.delete_failed,
if (deleted) FlashType.SUCCESS else FlashType.ERROR
)
flashMessage(
if (deleted) R.string.deleted else R.string.delete_failed,
if (deleted) FlashType.SUCCESS else FlashType.ERROR,
)

if (!deleted) {
return@executeAsync
}
if (!deleted) {
return@executeAsync
}

notifyFileDeleted(file, context)
notifyFileDeleted(file, context)

if (lastHeld != null) {
val parent = lastHeld.parent
parent.deleteChild(lastHeld)
requestExpandNode(parent)
} else {
requestFileListing()
}
if (lastHeld != null) {
val parent = lastHeld.parent
parent.deleteChild(lastHeld)
requestExpandNode(parent)
} else {
requestFileListing()
}

val frag = context.getEditorForFile(file)
if (frag != null) {
context.closeFile(context.findIndexOfEditorByFile(frag.file))
}
}
}
.setTitle(R.string.title_confirm_delete)
.setMessage(
context.getString(
R.string.msg_confirm_delete,
String.format("%s [%s]", file.name, file.absolutePath)
)
)
.setCancelable(false)
.showWithLongPressTooltip(
context = context,
tooltipTag = TooltipTag.PROJECT_CONFIRM_DELETE
)
}
val frag = context.getEditorForFile(file)
if (frag != null) {
context.closeFile(context.findIndexOfEditorByFile(frag.file))
}
}
}.setTitle(R.string.title_confirm_delete)
.setMessage(
context.getString(
R.string.msg_confirm_delete,
String.format("%s [%s]", file.name, file.absolutePath),
),
).setCancelable(false)
.showWithLongPressTooltip(
context = context,
tooltipTag = TooltipTag.PROJECT_CONFIRM_DELETE,
)
}

private fun notifyFileDeleted(file: File, context: Context) {
val deletionEvent = FileDeletionEvent(file)
private fun notifyFileDeleted(
file: File,
context: Context,
) {
val deletionEvent = FileDeletionEvent(file)

// Notify FileManager first
FileManager.onFileDeleted(deletionEvent)
// Notify FileManager first
FileManager.onFileDeleted(deletionEvent)

EventBus.getDefault().post(deletionEvent.putData(context))
}
EventBus.getDefault().post(deletionEvent.putData(context))
}
}
Loading
Loading