Skip to content

Commit f13ddd6

Browse files
hal-eisen-adfayaturnerclaudejimturner-adfa
authored
ADFA-4928 create a single manager for plugins and templates (appdevforall#1627)
* ADFA-4928: Wire up Jetpack Compose in the app module Adds the Compose plugin/buildFeatures/dependencies to app/build.gradle.kts, mirroring the floating-window/profiler modules' setup, plus a shared ManagerTheme composable that resolves Theme.AndroidIDE's Material3 attrs (same technique as FloatingTheme). This is the first commit of the Plugin Manager + Template Manager merge (ADR 0009 requires new screens to be Compose); the theme/build wiring lands separately from any screen code so it's independently reviewable and buildable. * ADFA-4928: Port Plugin Manager screen to Compose Rebuilds PluginManagerActivity's screen in Jetpack Compose (ADR 0009), preserving every capability of the old RecyclerView/dialogs UI: install via SAF picker, enable/disable/uninstall, overwrite and signature-mismatch conflict handling, restart prompt, and the discover-plugins action. PluginManagerViewModel/PluginRepository are reused unchanged. The six long-press tooltip anchor points collapse to two (list items, and the screen's background/empty state) since they all showed the same TooltipTag.PLUGIN_MANAGER content anyway - verified on-device that the long-press still correctly reaches TooltipManager. Also moves two dialogs' hardcoded English strings (uninstall confirmation, plugin details labels) into string resources. Note: taken together with the prior commit, this is the buildable/ tested state; the prior commit's PluginListAdapter.kt deletion was accidentally bundled with the build-wiring commit rather than this one, so that earlier commit alone doesn't compile in isolation - only the combined history does (verified via :app:assembleV8Debug and a manual on-device pass). * ADFA-4928: Add Templates data layer Ports the parsing/model layer from appdevforall/TemplateManagerPlugin (CgtTemplateReader, TemplateMetadata/CgtFileItem, plus their unit tests) into the app module as the basis for the new Templates tab. Adds TemplateRepository/TemplateRepositoryImpl, which reimplement the plugin's install/uninstall/delete semantics as direct file operations on Environment.TEMPLATES_DIR + the Downloads folder, since the host app doesn't need IdeTemplateService's plugin-facing permission gate. Provenance (bundled/plugin/user) is inferred from the same filename convention IdeTemplateServiceImpl/PluginProjectManager already use. Adds TemplateManagerViewModel (UDF shape matching PluginManagerViewModel) and a Koin di/TemplateModule, registered in IDEApplication alongside pluginModule. No UI yet - this commit is data-layer only. CgtTemplateReaderTest needs @RunWith(RobolectricTestRunner::class): org.json.JSONObject throws "not mocked" under a plain JVM unit test, same as other app-module tests that touch real android.jar classes. * ADFA-4928: Add Templates tab Compose UI Adds the Compose UI for the Templates tab, backed by the data layer from the previous commit: TemplateListItem (card - tapping only opens the multi-template sub-list, matching the reference plugin's design), TemplateManagerDialogs (delete confirmation, file-level details, per-template details, multi-template sub-list), and TemplateManagerScreen (content composable wiring the ViewModel's uiState/uiEffect, same long-press pointerInput tooltip shim as the Plugins tab, new TooltipTag.TEMPLATE_MANAGER). TemplateManagerScreen is content-only (no Scaffold/TopAppBar/FAB) - unlike the Plugins tab there's no install-flow FAB, matching the ported plugin's passive Downloads-folder scanning. It's meant to be composed as one tab's body inside the shared manager screen; wiring the two tabs together is the next commit. * ADFA-4928: Wire Plugins and Templates tabs together New ManagerScreen composable owns the shared Scaffold/TopAppBar/TabRow + HorizontalPager, hosting Plugins and Templates as pages (Plugins default). The FAB and discover-plugins action only render on the Plugins tab, since Templates is a passive Downloads-folder scan with no equivalent action. Refactors the old PluginManagerScreen into PluginManagerContent - a Scaffold-free content composable, matching TemplateManagerScreen's shape - so both tabs plug into ManagerScreen's single Scaffold instead of nesting their own. PluginManagerActivity now resolves both PluginManagerViewModel and TemplateManagerViewModel and renders ManagerScreen; its class name and entry points (Settings, the crash-recovery dialog) are unchanged. Updates ARCHITECTURE.md: this is the first production Compose screen in app (ADR 0009), and templates/manager is a new data-layer package. Verified end-to-end on a physical device: assembleV8Debug, installed APK, exercised both tabs from Settings -> Plugin Manager. Templates tab correctly scanned Environment.TEMPLATES_DIR + Downloads (found real pre-existing .cgt fixtures on the test device), and a full install/uninstall round-trip moved files between Downloads and TEMPLATES_DIR and refreshed the list correctly. No crashes. * ADFA-4928: Complete tab wiring (PluginManagerContent refactor + activity + docs) Finishes the previous commit: a staging mistake (a `git add` call hit a stale pathspec and aborted before reaching these files) left `81e3797ab` with only the new `ManagerScreen.kt` and a content-less file rename, referencing a `PluginManagerContent` composable that didn't exist yet in that commit alone - not independently buildable. This commit adds what was missed: the actual `PluginManagerContent.kt` refactor (Scaffold/TopAppBar/FAB stripped out, now content-only), `PluginManagerActivity.kt` wired to render `ManagerScreen` with both view models, the `ARCHITECTURE.md` updates, and the `title_manager` string. Combined history through this commit compiles (:app:compileV8DebugKotlin) and matches what was already verified end-to-end on-device in the previous message. * ADFA-4928: Rename the preferences entry to Extensions Manager The Settings entry that opens the merged Plugins/Templates screen was still titled "Plugin Manager" with a summary mentioning "extensions" (the old plugin-only wording). Renamed to "Extensions Manager" with a summary reflecting both tabs it now opens: "Manage IDE plugins and templates". Verified on-device: preferences list and the opened screen both render correctly. * ADFA-4928: Warm Application.filesDir off the main thread PluginModule's Koin factories called Context.filesDir directly, which does a real File.exists() check on every call, not just the first. That trips StrictMode's DiskReadViolation the first time the Extensions Manager screen resolves PluginRepository/PluginManagerViewModel on the main thread. Cache the resolved File once, off-main, during app startup (IDEApplication.cachedFilesDir), and have PluginModule read that instead - later reads are then a plain field access rather than a syscall. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ADFA-4928: Narrow the plugin install file picker to .cgp-like files The SAF picker launched with "*/*", showing every file regardless of type. SAF filters by MIME, not extension, and .cgp has no registered MIME type, so the closest working filter is "application/octet-stream" - what document providers report for files with an unrecognized extension. This hides files with a known type (zips, jars, images, ...) while leaving .cgp files selectable. isSupportedPluginFile() still validates the actual pick, since this is an approximation, not an exact extension filter (SAF has no such thing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ADFA-4928: Harden TemplateRepositoryImpl file operations Address CodeRabbit review feedback on PR appdevforall#1627: - Use SLF4J logging instead of android.util.Log - Narrow runCatching to expected I/O/parsing exceptions, rethrowing CancellationException instead of swallowing it - Refuse to install/uninstall over an existing same-name destination file instead of silently overwriting it - Treat a failed source-file delete as an install/uninstall failure and roll back the copied destination file * ADFA-4928: Keep plugin picker/install work off the main thread Address CodeRabbit review feedback on PR appdevforall#1627: - Warm IDEApplication.cachedFilesDir on an IO thread before Koin starts, eliminating the race where pluginModule/templateModule could resolve it on the main thread first - Bound FileImage's bitmap decode with inSampleSize and move the file-existence check inside the IO dispatcher; narrow its catch to recoverable failures and let CancellationException propagate - Move the picked plugin file's name/extension validation (a ContentResolver IPC call for content:// URIs) off the picker callback and into PluginManagerViewModel on a background dispatcher, routed back through a new ShowInstallConfirmation effect - Replace android.util.Log with SLF4J logging in PluginManagerContent - Narrow the file-picker launch catch to ActivityNotFoundException and log it instead of silently swallowing any Exception * ADFA-4928: Fix manager UI correctness/accessibility issues Address CodeRabbit review feedback on PR appdevforall#1627: - Avoid double system-bar insets by zeroing ManagerScreen's Scaffold contentWindowInsets, since the activity's root already applies them - Fix the back button's TalkBack announcement (was "Cancel") with a dedicated cd_navigate_back string - Wire long-press tooltips to the discover-plugins action and install FAB - Always show Uninstall for a listed plugin, even when it failed to load, so a broken plugin has a recovery action - Move the detail-row "label: value" format into a string resource so translators control ordering/punctuation - Only treat a template card as clickable when it bundles more than one template, instead of always exposing tap/press semantics - Use an Android plurals resource for the template count string instead of a fixed "templates" string - Buffer TemplateManagerViewModel's uiEffect channel and use send() instead of trySend() so effects aren't dropped before a collector is ready * ADFA-4928: Add KDoc for TemplateMetadata/CgtFileItem Address CodeRabbit review feedback on PR appdevforall#1627: document the model contracts, including the meaning of installed/provenance and the one-archive-to-many-templates relationship. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ADFA-4928: Enable JUnit Jupiter for app unit tests Address CodeRabbit review feedback on PR appdevforall#1627 (matches the JUnit Jupiter + Truth strategy ARCHITECTURE.md already documents for unit tests, which the app module hadn't wired up yet): - Run app unit tests on the JUnit Platform, with the vintage engine so existing JUnit 4/Robolectric tests keep running unchanged - Migrate CgtFileItemTest (no Robolectric dependency) to org.junit.jupiter.api.Test with Truth assertions - Keep CgtTemplateReaderTest on JUnit 4/RobolectricTestRunner (no built-in Jupiter integration) but switch its assertions to Truth Verified all 22 app unit test classes still run under :app:testV8DebugUnitTest with 0 failures. * ADFA-4928: Fix regressions CodeRabbit's re-review found in prior fixes Address CodeRabbit follow-up review feedback on PR appdevforall#1627: - Only run the cachedFilesDir warmup eagerly in onCreate() when credential-protected storage is already unlocked - the default Context.getFilesDir() throws during Direct Boot. When locked, warm it instead from CredentialProtectedApplicationLoader.load(), which only proceeds once that storage is confirmed accessible. - Base FileImage's inSampleSize loop on the larger image dimension instead of requiring both dimensions to exceed the target, so a wide-but-short (or tall-but-narrow) image still gets downsampled - Log FileImage's swallowed SecurityException/OutOfMemoryError icon-load failures via a throttled SLF4J warning, without logging the file path - Buffer PluginManagerViewModel's uiEffect channel and use send() instead of trySend(), same fix already applied to TemplateManagerViewModel, so effects (e.g. the new ShowInstallConfirmation) aren't dropped - Narrow UriExtensions.getFileName's second catch to SecurityException/ IllegalArgumentException instead of blanket Exception, so unexpected ContentResolver failures surface instead of being silently mislabeled as "Unknown File" (and then downstream as an unsupported plugin file); switch its logging to a class-scoped SLF4J logger * ADFA-4928: Address human review comments from hal-eisen-adfa - Disable the install FAB while a plugin install is in flight, so a second tap can't start a concurrent installPlugin() coroutine. The Compose ManagerScreen replaced the old Activity, which disabled the FAB via binding.fabInstallPlugin.isEnabled = !state.isInstalling; nothing carried that behavior over. - Fix PLUGIN_AUTHORING.md pointers left dangling by the PluginListAdapter.kt -> PluginListItem.kt/FileImage.kt migration. The delete-failure-handling and cachedFilesDir warmup comments from the same review were already addressed by prior commits on this branch; verified against current HEAD, no further changes needed. * ADFA-4928: Fix double system-bar insets and non-lifecycle-scoped effect collection - ManagerScreen's TopAppBar still used its default status-bar insets on top of PluginManagerActivity.onApplySystemBarInsets, which already pads the root view by the full system-bar insets (that padding doesn't consume the insets, so Compose saw them a second time). Zero out TopAppBar's windowInsets to match the Scaffold's contentWindowInsets, which was already zeroed. - PluginManagerContent and TemplateManagerScreen collected viewModel.uiEffect in a bare LaunchedEffect, so it kept collecting while the activity was stopped. A plugin install finishing while the app is backgrounded could then run DialogUtils.showRestartPrompt or a flashbar builder against a stopped activity. Wrap both collectors in repeatOnLifecycle(STARTED), matching the old repeatOnLifecycle(STARTED) pattern used elsewhere in the app. * ADFA-4928: Address second review pass from hal-eisen-adfa - Long-press tooltips on the FAB and Discover-plugins IconButton never fired: pointerInput(detectTapGestures) placed on the caller-side modifier loses the down event to the button's own internal clickable, which runs first on the Main pointer pass. Drive the tooltip off the button's own MutableInteractionSource instead (press duration vs LocalViewConfiguration's longPressTimeoutMillis), which observes the same press stream the button already dispatches rather than racing it for the raw pointer event. - CgtTemplateReader.readTemplates read a zip entry's bytes unbounded, so a corrupt/hostile .cgt sitting in the public Downloads folder could OOM the app; bound the read and throw IOException past 1 MiB. parseCgtFile also didn't catch IllegalArgumentException, which ZipInputStream.nextEntry throws for a non-UTF-8 entry name - that propagated out of the bare viewModelScope.launch in TemplateManagerViewModel.loadTemplates (no CoroutineExceptionHandler) and crashed the app. Both are now handled per-file, so one bad archive is skipped instead of failing the whole scan. - UriExtensions.getFileName's catch was narrowed to SecurityException/IllegalArgumentException, but a misbehaving content provider can throw other RuntimeExceptions from query()/getString() (CursorWindowAllocationException, a wrapped DeadObjectException, ...). Broadened back to Exception, since this is a best-effort display-name lookup, not a path that should ever crash the caller. - TemplateManagerDialogs' DetailRow still built "$label: $value" with string concatenation instead of the R.string.label_value fix that landed in the plugin dialog, and the optional-tags list hardcoded a non-ASCII "*" bullet in code (CLAUDE.md's ASCII rule). Added R.string.template_optional_tag and reused R.string.label_value. TemplateListItem's status/provenance row had the same hardcoded-separator shape; extracted it to R.string.label_separator. - PluginManagerActivity's try/catch around setContent no longer caught anything: setContent only registers the composable, and its lambda (where both ViewModels first resolve via Koin) runs at first layout, after onCreate has already returned past the catch. Force-resolve both `by viewModel()` delegates inside the try, before setContent. - PluginListItem.pluginVersionLabel duplicated CgtFileItem.versionLabel and disagreed with it on blank input (a stray "v" chip vs the tested ""). Deleted the duplicate and reused the tested helper. Added a CgtTemplateReaderTest regression case covering the bounded-read cap. Verified via :app:testV8DebugUnitTest (all passing) and spotlessCheck. * ADFA-4928: Guard FileImage's downsampling loop against a non-positive bound CodeRabbit flagged (2026-08-05 review, still unresolved) that decodeBounded()'s inSampleSize loop assumes maxDimensionPx > 0. If it's ever <= 0 - e.g. the 40.dp default rounding to a sub-pixel size at an unusual density - the loop condition (a non-negative quotient >= a non-positive bound) is permanently true, hanging on an unbounded doubling of inSampleSize instead of throwing. Skip the downsampling loop entirely in that case and decode at inSampleSize = 1. * ADFA-4928: Address third review pass from hal-eisen-adfa - Wrap the cachedFilesDir warm-up in runCatching in both IDEApplication.onCreate and CredentialProtectedApplicationLoader.load, so a filesDir failure unrelated to Direct Boot lock state (ADFA-2358) degrades to a disk read on first use instead of crashing the process. - Replace the Discover-plugins IconButton with a Box+combinedClickable so a single gesture detector owns long-press and click, and make the FAB consume a one-shot suppression flag set at the long-press timeout, so long-pressing either control shows the tooltip without also firing its click action. Verified on-device: long-press shows the tooltip and leaves the file picker/browser closed; a normal tap still opens each. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ADFA-4928: Address fourth review pass from hal-eisen-adfa - Fix the FAB long-press suppression flag latching and eating a later, unrelated tap: reset it at press start instead of relying on the click to clear it, so a long press that never ends in a tap-up on the FAB (slide-off, or the tooltip popup stealing the gesture) can no longer swallow the next real Install tap. Blocking. - Restore accessibility parity the IconButton -> Box swap dropped for the Discover-plugins control: role = Role.Button and onLongClickLabel so TalkBack again announces it as a button and names the long-press action. - Broaden the plugin file-picker SAF filter to application/octet-stream, application/zip, */* - some providers report a .cgp as application/zip rather than octet-stream, which the single-type filter hid with no way to reach them. - Delete the dead TemplateOperation sealed class (zero references). - Wire TemplateManagerUiState.isLoading through installTemplate/ uninstallTemplate/confirmDeleteDownloadFile (previously only loadTemplates set it) and render it as a top-aligned LinearProgressIndicator, so install/ uninstall/delete get visible feedback between the tap and the flashbar. - Make the Templates and Plugins tab dialog state rememberSaveable, so rotating or switching tabs (HorizontalPager disposes the off-screen page's state) no longer silently dismisses an open confirmation dialog. Neither CgtFileItem nor PluginInfo is Parcelable, so state is keyed on the file path / plugin id and the live item is resolved from uiState at the point of use - this avoids adding Parcelable to plugin-api's public API surface. - Add TemplateRepositoryImpl tests pinning the two riskiest branches: a name collision must fail without touching either copy, and a failed delete after a successful copy must roll back to leave exactly one copy behind (both for installTemplate and uninstallTemplate). Add TemplateManagerViewModel tests covering the init load, install success/failure, and the effect buffering Channel.BUFFERED was chosen to protect. Not changed: the two threads Hal already marked resolved (cachedFilesDir runCatching, FAB/Discover long-press-vs-click) needed no further action. The template.manager/plugin.manager tooltip-body question is answered in a PR reply - the bundled documentation.db has neither tag, pre-existing and not fixable from this repo (the asset is fetched from an external URL, not seeded here). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ADFA-4928: Offer the add-extension button on both manager tabs The FAB and the discover-plugins action were both gated on the Plugins tab, justified by the Templates tab being "a passive scan of the Downloads folder with no equivalent action". Merging stage falsified that: ADFA-4934 brought in TemplateCollectionRepository and a tested .cgt install flow (confirm -> name conflict -> overwrite/rename), reachable until now only by opening a file from outside the app. The screen is the Extensions Manager, so one "+" means "add an extension" on either tab. The picked file is routed by extension and the owning tab is brought forward, so the result is visible where it landed. Discover stays Plugins-only - it opens a plugin catalog, which has no meaning on the Templates tab. - Move the SAF launcher from PluginManagerContent up to ManagerScreen. HorizontalPager disposes the off-screen page, so a launcher owned by the Plugins page would not exist while Templates is showing. Routing also brings the target tab forward *before* dispatching, because uiEffect is a receiveAsFlow() Channel with a single consumer that lives on that page. - Extract ExternalFileInstallDialogs from ExternalFileInstallScreen, so the manager reuses ADFA-4934's confirm/conflict/rename flow rather than growing a second one. The activity keeps its own finish behaviour via onFinish. - Keep .cgp on the existing ContentUri path instead of routing it through onReceived. That path hands the ViewModel a LocalFile temp copy, for which the install dialog's "delete installation file after install" checkbox is meaningless - picked plugins would have silently lost that option. - Drop the now-dead OpenFilePicker event, effect and openFilePicker(). - Add msg_unsupported_extension_file for a pick that is neither type. Verified on the emulator: the "+" is present on both tabs and Discover is not; picking a .cgt stays on Templates and opens the collection-install dialog naming all nine templates in the archive; picking a .cgp brings the Plugins tab forward with the plugin dialog and its delete-source checkbox; picking a .exp shows the new message. * ADFA-4928: Quote the archive name with backticks in the install message msg_template_installed was written as "%1$s" installed successfully, but Android strips unescaped double quotes from a string value, so the quotes never reached the screen - it rendered as `qa-hello installed successfully` with the name unquoted. Backticks are not stripped, and strings.xml is already inconsistent about quoting %1$s, so this avoids adding another escaped-quote variant. Verified on the emulator: the flashbar now reads `qa-hello` installed successfully. * ADFA-4928: Show one card per template archive, not one per file on disk scanTemplates() concatenated the two directory scans with no de-duplication, so a .cgt present in both the template store and Downloads produced two cards. They render identically apart from the status line - same title (which is the first bundled template's name, not the archive's), same filename, and neither shows a location - and the Downloads twin is a dead end, since installTemplate refuses to overwrite and its Install can only ever fail. Let the installed copy win. Names are compared case-insensitively to match the stricter of the two install paths (TemplateCollectionRepository .findExistingCollision), so any row still listed as "not installed" is one the user can actually install. Filtering happens before parsing, so a shadowed archive is not unzipped just to be discarded. Tests move to Robolectric: the new cases build real .cgt archives, and parsing one reaches org.json.JSONObject, a "not mocked" stub under plain android.jar - the same reason CgtTemplateReaderTest already uses it. Added cases cover the twin being hidden, case-insensitive matching, and - the one that catches a sloppy filter - downloads that are not twins surviving. Known interaction: with the twin hidden, uninstallTemplate still refuses while a same-named file sits in Downloads, and that file no longer has a row. The failure names it exactly ("A download named 'x.cgt' already exists in /storage/emulated/0/Download"), so it is recoverable; changing uninstall's semantics is deliberately left out of this change. Verified on the emulator by reproducing the reported state - qa-hello.cgt installed and a second copy pushed to Downloads - which previously showed two identical cards and now shows one, marked Installed. --------- Co-authored-by: yaturner <thursdaynext@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: jimturner-adfa <jim.turner@appdevforall.org>
1 parent 0596292 commit f13ddd6

40 files changed

Lines changed: 3211 additions & 807 deletions

‎ARCHITECTURE.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66

77
Code On The Go (CoGo) is a full Android IDE that runs **on the device** — it edits, builds, and deploys real Android apps offline, embedding a Termux toolchain and running an actual Gradle build in a separate process via the `tooling-api`. It is the maintained successor to AndroidIDE, so the codebase namespace is still `com.itsaky.androidide`.
88

9-
There is **no single architectural philosophy** across the whole app. This large, layered application is still **predominantly View-based**: newer feature surfaces (plugin manager, AI agent, git, project list) follow a deliberate **Unidirectional Data Flow (UDF)** with Koin DI, `ViewModel` + `StateFlow`, sealed UI-state/effect types, and repositories, while older surfaces still use `LiveData` and talk to GreenRobot EventBus directly. New work follows the UDF pattern documented below, and new UI is built in **Jetpack Compose** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)) — Compose replaces the view layer only; the UDF stack (ViewModel + `StateFlow`, Koin, repositories) is unchanged. Existing XML/View screens remain until substantially reworked.
9+
There is **no single architectural philosophy** across the whole app. This large, layered application is still **predominantly View-based**: newer feature surfaces (plugin manager, AI agent, git, project list) follow a deliberate **Unidirectional Data Flow (UDF)** with Koin DI, `ViewModel` + `StateFlow`, sealed UI-state/effect types, and repositories, while older surfaces still use `LiveData` and talk to GreenRobot EventBus directly. New work follows the UDF pattern documented below, and new UI is built in **Jetpack Compose** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)) — Compose replaces the view layer only; the UDF stack (ViewModel + `StateFlow`, Koin, repositories) is unchanged. Existing XML/View screens remain until substantially reworked. The first production example is the **Manager** screen (`PluginManagerActivity`) — merged Plugins/Templates tabs built with `Scaffold`/`TabRow`/`HorizontalPager` (ADFA-4928).
1010

1111
## Core Architecture & Data Flow
1212

13-
Feature code layers as **UI → ViewModel → Repository → data source**, with state flowing up and events/intents flowing down. Koin provides dependencies (`coreModule`, `pluginModule`), constructor-injected into ViewModels.
13+
Feature code layers as **UI → ViewModel → Repository → data source**, with state flowing up and events/intents flowing down. Koin provides dependencies (`coreModule`, `pluginModule`, `templateModule`), constructor-injected into ViewModels.
1414

1515
- **Data sources** — Room (`RecentProjectRoomDatabase` + DAO, `suspend` functions), raw SQLite (`SQLiteOpenHelper`, e.g. `localWebServer/WebServer`), the filesystem/preferences, the embedded `tooling-api` (on-device Gradle), and external clients (Gemini via the Google GenAI SDK, on-device llama.cpp, JGit). Most are exposed through `suspend` functions.
16-
- **Repositories** — e.g. `agent/repository/GeminiRepository`, `repositories/PluginRepository`, `repositories/BreakpointRepository`. They wrap data sources and hide threading/IO from the ViewModel.
16+
- **Repositories** — e.g. `agent/repository/GeminiRepository`, `repositories/PluginRepository`, `repositories/TemplateRepository`, `repositories/BreakpointRepository`. They wrap data sources and hide threading/IO from the ViewModel.
1717
- **ViewModels** — run work in `viewModelScope` on `Dispatchers.IO`, hold a private `MutableStateFlow`/`MutableSharedFlow`, and expose read-only `StateFlow`/`SharedFlow`. One-shot effects (toasts, navigation, dialogs) go through a separate `SharedFlow` of a sealed `*UiEffect` type.
18-
- **UI (Fragments / Activities / Views)** — collect state in a lifecycle-aware coroutine and render it; user actions return to the ViewModel as method calls or sealed `*UiEvent` intents. The existing UI is **Android Views + Fragments + RecyclerView adapters**; new UI is Jetpack Compose ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)). (`compose-preview` previews the *user's* Compose code, not CoGo's own.)
18+
- **UI (Fragments / Activities / Views)** — collect state in a lifecycle-aware coroutine and render it; user actions return to the ViewModel as method calls or sealed `*UiEvent` intents. The existing UI is **Android Views + Fragments + RecyclerView adapters**; new UI is Jetpack Compose ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)) — first used in the Manager screen (`ui/compose/ManagerScreen.kt`, ADFA-4928). (`compose-preview` previews the *user's* Compose code, not CoGo's own.)
1919

2020
```
2121
┌─────────────────────────────────────────────┐
@@ -83,14 +83,14 @@ These structural facts shape every module. Day-to-day build *commands* live in `
8383
- **SDK levels** (`build-logic/.../build/config/BuildConfig.kt`): `COMPILE_SDK=36`, `MIN_SDK=28`, `TARGET_SDK=28`. **`TARGET_SDK` is deliberately pinned at 28:** higher targets enforce W^X (write-xor-execute), which blocks executing code from app-writable files. That is fatal for an on-device IDE that compiles and runs code (Gradle, `javac`, Termux binaries), so it is a hard requirement, not tech debt. `MIN_SDK_FOR_APPS_BUILT_WITH_COGO=16` is the floor for the apps a *user* builds with CoGo — distinct from CoGo's own `MIN_SDK`.
8484
- **Native asset bundling.** The on-device LLM (`llama-impl`) ships as a per-flavor native AAR, wired through the root `build.gradle.kts` (`bundleLlamaV8Assets` / `assembleV8Assets`, …); prebuilt per-flavor assets live under `assets/release/v7/` and `assets/release/v8/`.
8585
- **Native lib compression** (ADFA-2306, ADFA-4729). The app manifest hard-codes `android:extractNativeLibs="true"` (required: the installer must materialize libs in `nativeLibraryDir`, e.g. `libshizuku.so` is an executable the adb shell runs from there). That attribute overrides the `jniLibs.useLegacyPackaging` DSL, so AGP packages `lib/<abi>/*.so` deflate-compressed in **every** APK — ~5.9 MB smaller (`libtree-sitter-kotlin.so` alone is 4.18 MB → 339 kB). The trap is the `recompressApk` post-step (release always, debug in CI only): its no-compress lists in `app/build.gradle.kts` must NOT contain `"so"`, or it silently re-stores the libs and undoes the saving — which is what ADFA-2306 fixed for release and ADFA-4729 for CI debug. Locally built debug APKs (including the e2e farm's) never run that step and were always fine.
86-
- **`app` package layout is by concern, not feature:** `activities`, `fragments`, `services`, `di`, `agent`, `viewmodel(s)`, `repositories`, `roomData`, `localWebServer`, `preferences`, `ui`, `utils`, ….
86+
- **`app` package layout is by concern, not feature:** `activities`, `fragments`, `services`, `di`, `agent`, `viewmodel(s)`, `repositories`, `roomData`, `localWebServer`, `preferences`, `ui` (Compose screens live under `ui/compose`), `templates/manager` (the Manager screen's `.cgt`-parsing data layer, with direct filesystem access to `Environment.TEMPLATES_DIR` — distinct from the plugin-facing `IdeTemplateService` in `plugin-api`/`plugin-manager`), `utils`, ….
8787

8888
## Technology Stack
8989

9090
| Concern | Library / Approach |
9191
|---|---|
92-
| UI | **Jetpack Compose for all new UI** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)). The existing majority is still Android Views + Fragments + `RecyclerView` (Material Components); those legacy screens stay until reworked, but new IDE UI is Compose-only. |
93-
| Dependency Injection | **Koin** (`org.koin`) — `coreModule`/`pluginModule`, `startKoin` in `IDEApplication`, plus a `ServiceLocator : KoinComponent` for lazy post-startup access. No Hilt/Dagger. |
92+
| UI | **Jetpack Compose for all new UI** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)); first production screen is the Manager screen (Plugins/Templates tabs, `app/.../ui/compose/`, ADFA-4928). The existing majority is still Android Views + Fragments + `RecyclerView` (Material Components); those legacy screens stay until reworked, but new IDE UI is Compose-only. |
93+
| Dependency Injection | **Koin** (`org.koin`) — `coreModule`/`pluginModule`/`templateModule`, `startKoin` in `IDEApplication`, plus a `ServiceLocator : KoinComponent` for lazy post-startup access. No Hilt/Dagger. |
9494
| Asynchronous work | **Kotlin Coroutines + Flow** (`StateFlow`/`SharedFlow`, `viewModelScope`, app-scoped `CoroutineScope(SupervisorJob() + Dispatchers.IO)`); **GreenRobot EventBus** for cross-subsystem events. |
9595
| Networking | Offline-first; no general REST layer. External I/O is **Google GenAI SDK** (Gemini), **on-device llama.cpp**, and **JGit** (git). Retrofit is in the catalog but effectively unused in app code. |
9696
| Database / Persistence | **Room** is the default for relational/queryable data; **filesystem + preferences (DataStore)** for non-relational settings. **Raw SQLite** (`SQLiteDatabase` / `SupportSQLiteOpenHelper`) only for justified exceptions (see policy below). |

‎app/build.gradle.kts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ android {
9595
// Skip TreeSitter native library loading in tests
9696
it.systemProperty("java.library.path", System.getProperty("java.library.path"))
9797
it.systemProperty("androidide.test.mode", "true")
98+
// JUnit Platform, so JUnit Jupiter tests run; the vintage engine dependency
99+
// below keeps existing JUnit 4/Robolectric tests running unchanged.
100+
it.useJUnitPlatform()
98101
}
99102
}
100103
}
@@ -391,6 +394,10 @@ dependencies {
391394

392395
testImplementation(projects.testing.unit)
393396
testImplementation(libs.core.tests.anroidx.arch)
397+
testImplementation(libs.tests.junit.jupiter)
398+
testRuntimeOnly(libs.tests.junit.platformLauncher)
399+
// Keeps existing JUnit 4/Robolectric tests running under the JUnit Platform.
400+
testRuntimeOnly(libs.tests.junit.vintageEngine)
394401
androidTestImplementation(projects.common)
395402
androidTestImplementation(projects.testing.android) {
396403
exclude(group = "com.google.protobuf", module = "protobuf-lite")

‎app/src/main/java/com/itsaky/androidide/activities/ExternalFileInstallScreen.kt‎

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,26 +54,55 @@ private sealed interface DialogUiState {
5454
) : DialogUiState
5555
}
5656

57+
/**
58+
* Standalone host: the activity opened for a `.cgp`/`.cgt` from outside the app, which forwards
59+
* plugins to [PluginManagerActivity] and finishes itself when the flow ends.
60+
*/
61+
@Suppress("ktlint:compose:vm-forwarding-check")
5762
@Composable
5863
fun ExternalFileInstallScreen(viewModel: ExternalFileInstallViewModel) {
64+
val context = LocalContext.current
65+
ExternalFileInstallDialogs(
66+
viewModel = viewModel,
67+
onForwardPlugin = { filePath ->
68+
context.startActivity(
69+
Intent(context, PluginManagerActivity::class.java)
70+
// A Plugin Manager instance may already be running/backgrounded (e.g.
71+
// the user had it open, then opened a .cgp attachment) - these flags
72+
// reuse that instance via onNewIntent() instead of stacking a second
73+
// one on top of it.
74+
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
75+
.putExtra(PluginManagerActivity.EXTRA_PENDING_INSTALL_FILE_PATH, filePath),
76+
)
77+
(context as? Activity)?.finish()
78+
},
79+
onFinish = { (context as? Activity)?.finish() },
80+
)
81+
}
82+
83+
/**
84+
* The `.cgt` install flow's dialogs (confirm -> name conflict -> rename), driven by
85+
* [ExternalFileInstallViewModel]. Split out from [ExternalFileInstallScreen] so the Extensions
86+
* Manager's "add" button can reuse the same flow in-place instead of duplicating it: the only
87+
* host-specific behaviours are [onForwardPlugin] and [onFinish].
88+
*/
89+
@Composable
90+
fun ExternalFileInstallDialogs(
91+
viewModel: ExternalFileInstallViewModel,
92+
onForwardPlugin: (String) -> Unit,
93+
onFinish: () -> Unit,
94+
) {
5995
val context = LocalContext.current
6096
var dialogState by remember { mutableStateOf<DialogUiState>(DialogUiState.None) }
6197
val isInstalling by viewModel.isInstalling.collectAsStateWithLifecycle()
98+
val currentOnForwardPlugin by rememberUpdatedState(onForwardPlugin)
99+
val currentOnFinish by rememberUpdatedState(onFinish)
62100

63101
LaunchedEffect(viewModel) {
64102
viewModel.uiEffect.collect { effect ->
65103
when (effect) {
66104
is ExternalFileInstallUiEffect.ForwardToPluginManager -> {
67-
context.startActivity(
68-
Intent(context, PluginManagerActivity::class.java)
69-
// A Plugin Manager instance may already be running/backgrounded (e.g.
70-
// the user had it open, then opened a .cgp attachment) - these flags
71-
// reuse that instance via onNewIntent() instead of stacking a second
72-
// one on top of it.
73-
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
74-
.putExtra(PluginManagerActivity.EXTRA_PENDING_INSTALL_FILE_PATH, effect.filePath),
75-
)
76-
(context as? Activity)?.finish()
105+
currentOnForwardPlugin(effect.filePath)
77106
}
78107

79108
is ExternalFileInstallUiEffect.ShowTemplateInstallConfirmation -> {
@@ -101,7 +130,8 @@ fun ExternalFileInstallScreen(viewModel: ExternalFileInstallViewModel) {
101130
}
102131

103132
is ExternalFileInstallUiEffect.Finish -> {
104-
(context as? Activity)?.finish()
133+
dialogState = DialogUiState.None
134+
currentOnFinish()
105135
}
106136
}
107137
}

0 commit comments

Comments
 (0)