Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ There is **no single architectural philosophy** across the whole app. This large

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.

- **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.
- **Data sources** — Room (`RecentProjectRoomDatabase` + DAO, `suspend` functions), raw SQLite (`SQLiteOpenHelper`, e.g. `common/.../documentation/DocumentationContentSource`), 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.
- **Repositories** — e.g. `agent/repository/GeminiRepository`, `repositories/PluginRepository`, `repositories/TemplateRepository`, `repositories/BreakpointRepository`. They wrap data sources and hide threading/IO from the ViewModel.
- **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.
- **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.)
Expand Down Expand Up @@ -102,7 +102,7 @@ These structural facts shape every module. Day-to-day build *commands* live in `
>
> **Recent Projects** is the reference example of the default: `app/src/main/java/com/itsaky/androidide/roomData/recentproject/` (`RecentProjectRoomDatabase`, `@Database version = 4` with migrations 1→4; `RecentProjectDao`; the `RecentProject` `@Entity` → table `recent_project_table`). It's provided via Koin in `di/AppModule.kt` and consumed by `MainViewModel`, `RecentProjectsViewModel`, `MainActivity`, `ProjectInfoBottomSheet`, and `ProjectCreationManager`.
>
> **Raw SQLite is allowed only when** the database is prebuilt and opened read-only, the data is performance/allocation-critical and needs granular schema control, or the schema is shared across a process/component boundary. Current exceptions: symbol indexing (`lsp/indexing/SQLiteIndex.kt`), tooltips (`idetooltips/ToolTipManager.kt`), in-app/plugin help (`plugin-manager/.../documentation/PluginDocumentationManager.kt`), and the local web server (`app/.../localWebServer/WebServer.kt`). The `androidx.room:*` strings in `editor`'s `GroovyAutoComplete` are autocomplete suggestions for the *user's* code, not CoGo persistence.
> **Raw SQLite is allowed only when** the database is prebuilt and opened read-only, the data is performance/allocation-critical and needs granular schema control, or the schema is shared across a process/component boundary. Current exceptions: symbol indexing (`lsp/indexing/SQLiteIndex.kt`), tooltips (`idetooltips/ToolTipManager.kt`), in-app/plugin help (`plugin-manager/.../documentation/PluginDocumentationManager.kt`), and documentation serving (`common/.../documentation/DocumentationContentSource.kt`, the one pipeline behind both the in-process WebView transport and `app/.../localWebServer/WebServer.kt`). The `androidx.room:*` strings in `editor`'s `GroovyAutoComplete` are autocomplete suggestions for the *user's* code, not CoGo persistence.
>
> The tooltip, in-app/plugin-help, and local-web-server exceptions all read `documentation.db`, the prebuilt Tier 1/2/3 help database — see [docs/documentation-database.md](docs/documentation-database.md) for its schema and how each consumer queries it.

Expand Down
3 changes: 0 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,6 @@ dependencies {
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.runtime.ktx)
coreLibraryDesugaring(libs.desugar.jdk.libs.v215)

// Pebble template engine
implementation("io.pebbletemplates:pebble:4.1.1")
}

tasks.register("downloadDocDb") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,69 +17,84 @@

package com.itsaky.androidide.activities.editor

import androidx.core.graphics.Insets
import android.os.Bundle
import android.view.View
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import org.adfa.constants.CONTENT_KEY
import androidx.core.graphics.Insets
import com.itsaky.androidide.R
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityFaqBinding
import com.itsaky.androidide.documentation.DocumentationRequestInterceptor
import org.adfa.constants.CONTENT_KEY

class FAQActivity : EdgeToEdgeIDEActivity() {
@Suppress("ktlint:standard:backing-property-naming")
private var _binding: ActivityFaqBinding? = null
private val binding: ActivityFaqBinding
get() =
checkNotNull(_binding) {
"FAQActivity has been destroyed"
}

private var _binding: ActivityFaqBinding? = null
private val binding: ActivityFaqBinding
get() = checkNotNull(_binding) {
"FAQActivity has been destroyed"
}

override fun bindLayout(): View {
_binding = ActivityFaqBinding.inflate(layoutInflater)
return binding.root
}
override fun bindLayout(): View {
_binding = ActivityFaqBinding.inflate(layoutInflater)
return binding.root
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

with(binding) {
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(R.string.faq_activity_title)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
with(binding) {
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(R.string.faq_activity_title)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }

val htmlContent = intent.getStringExtra(CONTENT_KEY)
val htmlContent = intent.getStringExtra(CONTENT_KEY)

// htmlContent?.let {
// webView.clearCache(true)
// webView.loadDataWithBaseURL(null, it, "text/html", "UTF-8", null)
// }
// Enable JavaScript if required
webView.settings.javaScriptEnabled = true
// Enable JavaScript if required
webView.settings.javaScriptEnabled = true

// Set WebViewClient to handle page navigation within the WebView
webView.webViewClient = WebViewClient()
// Set WebViewClient to handle page navigation within the WebView. ADFA-5176: it answers
// documentation from the database in-process, falling through to the local web server
// for anything it declines.
webView.webViewClient =
object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? =
DocumentationRequestInterceptor.shared?.intercept(request)
?: super.shouldInterceptRequest(view, request)
}

// Load the HTML file from the assets folder
htmlContent?.let { webView.loadUrl(it) }
}
}
// Load the HTML file from the assets folder
htmlContent?.let { webView.loadUrl(it) }
}
}

override fun onApplySystemBarInsets(insets: Insets) {
val toolbar: View = binding.toolbar
toolbar.setPadding(
toolbar.paddingLeft + insets.left,
toolbar.paddingTop,
toolbar.paddingRight + insets.right,
toolbar.paddingBottom
)
override fun onApplySystemBarInsets(insets: Insets) {
val toolbar: View = binding.toolbar
toolbar.setPadding(
toolbar.paddingLeft + insets.left,
toolbar.paddingTop,
toolbar.paddingRight + insets.right,
toolbar.paddingBottom,
)

val webview: View = binding.webView
webview.setPadding(
webview.paddingLeft + insets.left,
webview.paddingTop,
webview.paddingRight + insets.right,
webview.paddingBottom
)
}
}
val webview: View = binding.webView
webview.setPadding(
webview.paddingLeft + insets.left,
webview.paddingTop,
webview.paddingRight + insets.right,
webview.paddingBottom,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,103 +24,113 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.itsaky.androidide.R
import com.itsaky.androidide.documentation.DocumentationRequestInterceptor


class IDETooltipWebviewFragment : Fragment() {
private lateinit var webView: WebView
private lateinit var website : String

//This warning is unnecessary because we control the content
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
Log.d(Companion.TAG, "IDETooltipWebviewFragment\\\\onCreateView called")
// Handle back press using OnBackPressedCallback
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
activity?.runOnUiThread {
webView.clearHistory()
webView.loadUrl("about:blank")
webView.destroy()
}
parentFragmentManager.popBackStack()
isEnabled =
false // Disable this callback to let the default back press behavior occur
}
}
})

website = arguments?.getString(MainFragment.KEY_TOOLTIP_URL).orEmpty()

val safeContext = ContextThemeWrapper(requireContext().applicationContext, requireContext().theme)
val view = LayoutInflater.from(safeContext).inflate(R.layout.fragment_idetooltipwebview, container, false)

webView = view.findViewById(R.id.IDETooltipWebView)

// Set a WebViewClient to handle loading pages
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
// Allow loading of local assets files
if (request.url.toString().startsWith("file:///android_asset/")) {
view.loadUrl(request.url.toString())
return true
}
return super.shouldOverrideUrlLoading(view, request)
}
}

// Set up WebChromeClient to support JavaScript
private lateinit var webView: WebView
private lateinit var website : String
// by lazy: an initializer here runs during Fragment construction on the main thread, where the
// interceptor's sentinel check is a disk read StrictMode reports and a missing database is a
// crash before the view exists. See HelpActivity for the same note.
private val documentation by lazy { DocumentationRequestInterceptor.shared }

//This warning is unnecessary because we control the content
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
Log.d(Companion.TAG, "IDETooltipWebviewFragment\\\\onCreateView called")
// Handle back press using OnBackPressedCallback
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
activity?.runOnUiThread {
webView.clearHistory()
webView.loadUrl("about:blank")
webView.destroy()
}
parentFragmentManager.popBackStack()
isEnabled =
false // Disable this callback to let the default back press behavior occur
}
}
})

website = arguments?.getString(MainFragment.KEY_TOOLTIP_URL).orEmpty()

val safeContext = ContextThemeWrapper(requireContext().applicationContext, requireContext().theme)
val view = LayoutInflater.from(safeContext).inflate(R.layout.fragment_idetooltipwebview, container, false)

webView = view.findViewById(R.id.IDETooltipWebView)

// Set a WebViewClient to handle loading pages
webView.webViewClient = object : WebViewClient() {
// ADFA-5176: documentation comes from the database in-process; anything this declines
// still goes to the local web server.
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? = documentation?.intercept(request) ?: super.shouldInterceptRequest(view, request)

override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
// Allow loading of local assets files
if (request.url.toString().startsWith("file:///android_asset/")) {
view.loadUrl(request.url.toString())
return true
}
return super.shouldOverrideUrlLoading(view, request)
}
}

// Set up WebChromeClient to support JavaScript
// webView.webChromeClient = WebChromeClient()
webView.settings.allowFileAccessFromFileURLs
webView.settings.allowFileAccess
webView.settings.allowUniversalAccessFromFileURLs
webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY
webView.scrollBarDefaultDelayBeforeFade = 1000


// Enable JavaScript if needed
webView.settings.javaScriptEnabled = true

// Load the HTML file from the assets folder
webView.loadUrl(website)
return view
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(Companion.TAG, "IDETooltipWebViewFragment\\\\onViewCreated called")
}

override fun onDestroyView() {
super.onDestroyView()
// Clean up the WebView in Fragment
if(webView.isVisible) {
webView.clearHistory()
webView.loadUrl("about:blank")
webView.destroy()
}

}

companion object {
private const val TAG = "IDETooltipWebViewFragment"
}
webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY
webView.scrollBarDefaultDelayBeforeFade = 1000


// Enable JavaScript if needed
webView.settings.javaScriptEnabled = true

// Load the HTML file from the assets folder
webView.loadUrl(website)
return view
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(Companion.TAG, "IDETooltipWebViewFragment\\\\onViewCreated called")
}

override fun onDestroyView() {
super.onDestroyView()
// Clean up the WebView in Fragment
if(webView.isVisible) {
webView.clearHistory()
webView.loadUrl("about:blank")
webView.destroy()
}

}

companion object {
private const val TAG = "IDETooltipWebViewFragment"
}


}
Loading
Loading