Skip to content

Commit 17a8f0b

Browse files
authored
Merge branch 'stage' into ADFA-4322
2 parents d6691b1 + eabb240 commit 17a8f0b

46 files changed

Lines changed: 1147 additions & 134 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/gh2jira.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
with:
2626
project: 'ADFA'
2727
issuetype: 'Story'
28-
summary: 'GitHub Issue: ${{ github.event.issue.title }}'
28+
summary: 'GHI: ${{ github.event.issue.title }}'
2929
description: |
3030
*GitHub Issue:* #${{ github.event.issue.number }}
3131
*URL:* ${{ github.event.issue.html_url }}

‎.mcp.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
"sentry": {
44
"type": "http",
55
"url": "https://mcp.sentry.dev/mcp"
6+
},
7+
"sonarqube": {
8+
"command": "sonar",
9+
"args": [
10+
"run",
11+
"mcp",
12+
"--project",
13+
"appdevforall_CodeOnTheGo"
14+
]
615
}
716
}
8-
}
17+
}

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import com.google.android.material.tabs.TabLayout.Tab
8282
import com.itsaky.androidide.FeedbackButtonManager
8383
import com.itsaky.androidide.R
8484
import com.itsaky.androidide.R.string
85+
import com.itsaky.androidide.activities.MainActivity
8586
import com.itsaky.androidide.actions.build.DebugAction
8687
import com.itsaky.androidide.adapters.DiagnosticsAdapter
8788
import com.itsaky.androidide.adapters.SearchListAdapter
@@ -111,6 +112,7 @@ import com.itsaky.androidide.models.Range
111112
import com.itsaky.androidide.models.SearchResult
112113
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
113114
import com.itsaky.androidide.preferences.internal.BuildPreferences
115+
import com.itsaky.androidide.preferences.internal.GeneralPreferences
114116
import com.itsaky.androidide.projects.IProjectManager
115117
import com.itsaky.androidide.projects.ProjectManagerImpl
116118
import com.itsaky.androidide.services.debug.DebuggerService
@@ -631,11 +633,35 @@ abstract class BaseEditorActivity :
631633
builder.show()
632634
}
633635

636+
/**
637+
* Restores the project path on recreation (saved state, launch intent, or last opened
638+
* project) and routes back to MainActivity if none is available, rather than crashing while
639+
* building the editor UI.
640+
*/
634641
override fun onCreate(savedInstanceState: Bundle?) {
635-
savedInstanceState?.getString(KEY_PROJECT_PATH)
636-
?.let(ProjectManagerImpl.getInstance()::projectPath::set)
642+
// The OS can recreate EditorActivity after process death without routing through
643+
// MainActivity, leaving the ProjectManagerImpl singleton's lateinit projectPath unset.
644+
// Restore it from the saved state, the launch intent, or the last opened project.
645+
val restoredProjectPath =
646+
savedInstanceState?.getString(KEY_PROJECT_PATH)?.takeIf { it.isNotBlank() }
647+
?: intent?.getStringExtra("PROJECT_PATH")?.takeIf { it.isNotBlank() }
648+
?: GeneralPreferences.lastOpenedProject
649+
.takeIf { it.isNotBlank() && it != GeneralPreferences.NO_OPENED_PROJECT }
650+
if (restoredProjectPath != null) {
651+
ProjectManagerImpl.getInstance().projectPath = restoredProjectPath
652+
}
637653
super.onCreate(savedInstanceState)
638654

655+
// If we still have no project path after every fallback, we cannot safely build the
656+
// editor UI (setupToolbar -> getProjectName dereferences the project path). Route the
657+
// user back to MainActivity instead of crashing.
658+
if (ProjectManagerImpl.getInstance().projectDirPath.isBlank()) {
659+
log.warn("No project path available in EditorActivity.onCreate(); returning to MainActivity")
660+
startActivity(Intent(this, MainActivity::class.java))
661+
finish()
662+
return
663+
}
664+
639665
editorViewModel.isBuildInProgress = false
640666
editorViewModel.isInitializing = false
641667

@@ -670,6 +696,7 @@ abstract class BaseEditorActivity :
670696

671697
setupStateObservers()
672698
setupFullscreenObserver()
699+
setupBottomSheetObserver()
673700
setupViews()
674701

675702
fullscreenManager = FullscreenManager(
@@ -1327,11 +1354,29 @@ abstract class BaseEditorActivity :
13271354
repeatOnLifecycle(Lifecycle.State.STARTED) {
13281355
editorViewModel.uiState.collectLatest { uiState ->
13291356
fullscreenManager?.render(uiState.isFullscreen, animate = true)
1357+
updateSwipeRevealDragState()
13301358
}
13311359
}
13321360
}
13331361
}
13341362

1363+
private fun setupBottomSheetObserver() {
1364+
lifecycleScope.launch {
1365+
repeatOnLifecycle(Lifecycle.State.STARTED) {
1366+
bottomSheetViewModel.sheetState.collectLatest { _ ->
1367+
updateSwipeRevealDragState()
1368+
}
1369+
}
1370+
}
1371+
}
1372+
1373+
private fun updateSwipeRevealDragState() {
1374+
val isFullscreen = editorViewModel.isFullscreen
1375+
val isBottomSheetOpen = bottomSheetViewModel.sheetBehaviorState != STATE_COLLAPSED &&
1376+
bottomSheetViewModel.sheetBehaviorState != STATE_HIDDEN
1377+
binding.swipeReveal.setVerticalDragEnabled(!isFullscreen && !isBottomSheetOpen)
1378+
}
1379+
13351380
private fun setupViews() {
13361381
setupNoEditorView()
13371382
setupBottomSheet()
@@ -1583,7 +1628,13 @@ abstract class BaseEditorActivity :
15831628
hasHorizontalVelocity &&
15841629
isHorizontalSwipe
15851630

1586-
// Fullscreen mode can be dismissed with an inward fling from either vertical edge.
1631+
val isBottomSheetOpen = bottomSheetViewModel.sheetBehaviorState != STATE_COLLAPSED &&
1632+
bottomSheetViewModel.sheetBehaviorState != STATE_HIDDEN
1633+
1634+
if (isBottomSheetOpen) {
1635+
return false
1636+
}
1637+
15871638
if (isTopEdgeDismissFling && editorViewModel.isFullscreen) {
15881639
editorViewModel.exitFullscreen()
15891640
return true

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

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.content.res.Configuration
2424
import android.os.Bundle
2525
import android.text.TextUtils
2626
import android.util.Log
27+
import android.util.TypedValue
2728
import android.view.KeyEvent
2829
import android.view.View
2930
import android.view.ViewGroup.LayoutParams
@@ -32,6 +33,8 @@ import androidx.collection.MutableIntObjectMap
3233
import androidx.core.content.res.ResourcesCompat
3334
import androidx.core.view.GravityCompat
3435
import androidx.core.view.doOnNextLayout
36+
import androidx.fragment.app.Fragment
37+
import androidx.fragment.app.FragmentManager
3538
import androidx.lifecycle.lifecycleScope
3639
import com.blankj.utilcode.util.ImageUtils
3740
import com.google.android.material.tabs.TabLayout
@@ -63,6 +66,7 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
6366
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
6467
import com.itsaky.androidide.activities.PluginManagerActivity
6568
import com.itsaky.androidide.eventbus.events.plugin.PluginCrashedEvent
69+
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
6670
import com.itsaky.androidide.idetooltips.TooltipManager
6771
import com.itsaky.androidide.idetooltips.TooltipTag
6872
import com.itsaky.androidide.interfaces.IEditorHandler
@@ -73,6 +77,7 @@ import com.itsaky.androidide.models.Range
7377
import com.itsaky.androidide.models.SaveResult
7478
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
7579
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
80+
import com.itsaky.androidide.preferences.internal.EditorPreferences
7681
import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
7782
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
7883
import com.itsaky.androidide.projects.ProjectManagerImpl
@@ -90,6 +95,7 @@ import com.itsaky.androidide.utils.EditorSidebarActions
9095
import com.itsaky.androidide.utils.IntentUtils.openImage
9196
import com.itsaky.androidide.utils.UniqueNameBuilder
9297
import com.itsaky.androidide.utils.flashSuccess
98+
import com.itsaky.androidide.utils.forEachViewRecursively
9399
import kotlinx.coroutines.Dispatchers
94100
import kotlinx.coroutines.NonCancellable
95101
import kotlinx.coroutines.launch
@@ -98,6 +104,7 @@ import org.adfa.constants.CONTENT_KEY
98104
import org.greenrobot.eventbus.Subscribe
99105
import org.greenrobot.eventbus.ThreadMode
100106
import java.io.File
107+
import java.util.WeakHashMap
101108
import java.util.concurrent.ConcurrentHashMap
102109
import java.util.concurrent.CopyOnWriteArrayList
103110
import java.util.concurrent.atomic.AtomicBoolean
@@ -125,6 +132,22 @@ open class EditorHandlerActivity :
125132

126133
private val pluginTabIndices = mutableMapOf<String, Int>()
127134
private val tabIndexToPluginId = mutableMapOf<Int, String>()
135+
private var lastAppliedPluginFontScale = EditorPreferences.editorFontScale
136+
private val pluginTextBaseSizes = WeakHashMap<TextView, Float>()
137+
138+
private val pluginFontScalingListener = object : FragmentManager.FragmentLifecycleCallbacks() {
139+
override fun onFragmentViewCreated(
140+
mFragmentManager: FragmentManager,
141+
mFragment: Fragment,
142+
view: View,
143+
savedInstanceState: Bundle?
144+
) {
145+
val scale = EditorPreferences.editorFontScale
146+
if (scale != 1f && isPluginFragment(mFragment)) {
147+
applyPluginFontScale(view, scale)
148+
}
149+
}
150+
}
128151
private val shortcutManager by lazy { ShortcutManager(applicationContext) }
129152

130153
private var pluginEditorProvider: EditorProviderImpl? = null
@@ -194,6 +217,8 @@ open class EditorHandlerActivity :
194217
mBuildEventListener.setActivity(this)
195218
super.onCreate(savedInstanceState)
196219

220+
supportFragmentManager.registerFragmentLifecycleCallbacks(pluginFontScalingListener, true)
221+
197222
editorViewModel._displayedFile.observe(
198223
this,
199224
) { fileIndex ->
@@ -392,25 +417,38 @@ open class EditorHandlerActivity :
392417
}
393418

394419
restoreOpenedPluginTabs()
420+
syncPluginUiFontSize()
395421
}
396422

423+
/**
424+
* Restores the plugin tabs cached from the previous session, running the
425+
* SharedPreferences IO and Gson decode off the main thread to avoid a startup UI stall.
426+
*/
397427
private fun restoreOpenedPluginTabs() {
398-
try {
399-
val prefs = (application as BaseApplication).prefManager
400-
val json = prefs.getString(PREF_KEY_OPEN_PLUGIN_TABS, null) ?: return
428+
lifecycleScope.launch {
429+
try {
430+
val prefs = (application as BaseApplication).prefManager
431+
val json = withContext(Dispatchers.IO) {
432+
prefs.getString(PREF_KEY_OPEN_PLUGIN_TABS, null)
433+
} ?: return@launch
401434

402-
val tabIds = Gson().fromJson(json, Array<String>::class.java)?.toList() ?: return
403-
Log.d("EditorHandlerActivity", "Restoring plugin tabs: $tabIds")
435+
// Decoding the cached JSON off the main thread avoids a UI stall on startup.
436+
val tabIds = withContext(Dispatchers.Default) {
437+
Gson().fromJson(json, Array<String>::class.java)?.toList()
438+
} ?: return@launch
439+
Log.d("EditorHandlerActivity", "Restoring plugin tabs: $tabIds")
404440

405-
tabIds.forEach { tabId ->
406-
if (!pluginTabIndices.containsKey(tabId)) {
407-
selectPluginTabById(tabId)
441+
// Tab selection touches UI state, so keep it on the main thread.
442+
tabIds.forEach { tabId ->
443+
if (!pluginTabIndices.containsKey(tabId)) {
444+
selectPluginTabById(tabId)
445+
}
408446
}
409-
}
410447

411-
prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, null)
412-
} catch (e: Exception) {
413-
Log.e("EditorHandlerActivity", "Failed to restore plugin tabs", e)
448+
withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, null) }
449+
} catch (e: Exception) {
450+
Log.e("EditorHandlerActivity", "Failed to restore plugin tabs", e)
451+
}
414452
}
415453
}
416454

@@ -1113,6 +1151,49 @@ open class EditorHandlerActivity :
11131151
showPluginCrashDialog(event)
11141152
}
11151153

1154+
@Subscribe(threadMode = ThreadMode.MAIN)
1155+
fun onPreferenceChanged(event: PreferenceChangeEvent) {
1156+
if (event.key == EditorPreferences.FONT_SIZE) {
1157+
syncPluginUiFontSize()
1158+
}
1159+
}
1160+
1161+
private fun syncPluginUiFontSize() {
1162+
val scale = EditorPreferences.editorFontScale
1163+
if (scale == lastAppliedPluginFontScale) {
1164+
return
1165+
}
1166+
lastAppliedPluginFontScale = scale
1167+
1168+
val pluginFragments = mutableListOf<Fragment>()
1169+
collectPluginFragments(supportFragmentManager, pluginFragments)
1170+
pluginFragments.forEach { fragment ->
1171+
fragment.view?.let { applyPluginFontScale(it, scale) }
1172+
}
1173+
}
1174+
1175+
private fun isPluginFragment(fragment: Fragment): Boolean =
1176+
fragment.javaClass.classLoader !== javaClass.classLoader
1177+
1178+
private fun applyPluginFontScale(root: View, scale: Float) {
1179+
root.forEachViewRecursively { view ->
1180+
if (view is TextView) {
1181+
val baseSize = pluginTextBaseSizes.getOrPut(view) { view.textSize }
1182+
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, baseSize * scale)
1183+
}
1184+
}
1185+
}
1186+
1187+
private fun collectPluginFragments(manager: FragmentManager, into: MutableList<Fragment>) {
1188+
manager.fragments.forEach { fragment ->
1189+
if (isPluginFragment(fragment)) {
1190+
into.add(fragment)
1191+
} else {
1192+
collectPluginFragments(fragment.childFragmentManager, into)
1193+
}
1194+
}
1195+
}
1196+
11161197
private fun showPluginCrashDialog(event: PluginCrashedEvent) {
11171198
val dialogView = layoutInflater.inflate(R.layout.dialog_plugin_crash, null)
11181199
dialogView.findViewById<TextView>(R.id.plugin_crash_message).text =

‎app/src/main/java/com/itsaky/androidide/agent/fragments/AgentFragmentContainer.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ class AgentFragmentContainer : EmptyStateFragment<FragmentAgentContainerBinding>
2121
super.onViewCreated(view, savedInstanceState)
2222
emptyStateViewModel.setEmptyMessage("No git actions yet")
2323
emptyStateViewModel.setEmpty(false)
24+
}
25+
26+
// Deferred to onResume so the disclaimer only fires when the Agent tab is
27+
// actually visible. ViewPager2 + FragmentStateAdapter pre-creates adjacent
28+
// pages through onViewCreated; only the visible page reaches onResume.
29+
override fun onResume() {
30+
super.onResume()
2431
showDisclaimerDialogIfNeeded()
2532
}
2633

0 commit comments

Comments
 (0)