Skip to content

Commit 664062d

Browse files
authored
Merge branch 'stage' into ADFA-3598
2 parents 9e4799d + fc5b6f9 commit 664062d

68 files changed

Lines changed: 2878 additions & 353 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.

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

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.content.Intent
2222
import android.content.ServiceConnection
2323
import android.content.res.Configuration
2424
import android.graphics.Color
25+
import android.graphics.Rect
2526
import android.graphics.drawable.GradientDrawable
2627
import android.os.Build
2728
import android.os.Bundle
@@ -82,6 +83,7 @@ import com.google.android.material.tabs.TabLayout.Tab
8283
import com.itsaky.androidide.FeedbackButtonManager
8384
import com.itsaky.androidide.R
8485
import com.itsaky.androidide.R.string
86+
import com.itsaky.androidide.activities.MainActivity
8587
import com.itsaky.androidide.actions.build.DebugAction
8688
import com.itsaky.androidide.adapters.DiagnosticsAdapter
8789
import com.itsaky.androidide.adapters.SearchListAdapter
@@ -111,6 +113,7 @@ import com.itsaky.androidide.models.Range
111113
import com.itsaky.androidide.models.SearchResult
112114
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
113115
import com.itsaky.androidide.preferences.internal.BuildPreferences
116+
import com.itsaky.androidide.preferences.internal.GeneralPreferences
114117
import com.itsaky.androidide.projects.IProjectManager
115118
import com.itsaky.androidide.projects.ProjectManagerImpl
116119
import com.itsaky.androidide.services.debug.DebuggerService
@@ -631,11 +634,35 @@ abstract class BaseEditorActivity :
631634
builder.show()
632635
}
633636

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

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

@@ -670,6 +697,7 @@ abstract class BaseEditorActivity :
670697

671698
setupStateObservers()
672699
setupFullscreenObserver()
700+
setupBottomSheetObserver()
673701
setupViews()
674702

675703
fullscreenManager = FullscreenManager(
@@ -1327,11 +1355,29 @@ abstract class BaseEditorActivity :
13271355
repeatOnLifecycle(Lifecycle.State.STARTED) {
13281356
editorViewModel.uiState.collectLatest { uiState ->
13291357
fullscreenManager?.render(uiState.isFullscreen, animate = true)
1358+
updateSwipeRevealDragState()
13301359
}
13311360
}
13321361
}
13331362
}
13341363

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

1586-
// Fullscreen mode can be dismissed with an inward fling from either vertical edge.
1632+
val isBottomSheetOpen = bottomSheetViewModel.sheetBehaviorState != STATE_COLLAPSED &&
1633+
bottomSheetViewModel.sheetBehaviorState != STATE_HIDDEN
1634+
1635+
if (isBottomSheetOpen) {
1636+
return false
1637+
}
1638+
15871639
if (isTopEdgeDismissFling && editorViewModel.isFullscreen) {
15881640
editorViewModel.exitFullscreen()
15891641
return true
@@ -1601,7 +1653,9 @@ abstract class BaseEditorActivity :
16011653
}
16021654

16031655
// Filter out diagonal flings so only an intentional right swipe opens the drawer.
1604-
if (isDrawerOpenFling) {
1656+
// A horizontal fling that started on the bottom-sheet tab strip is the user
1657+
// scrolling tabs, not asking for the drawer.
1658+
if (isDrawerOpenFling && !isTouchOnBottomSheetTabs(e1)) {
16051659
binding.editorDrawerLayout.openDrawer(GravityCompat.START)
16061660
return true
16071661
}
@@ -1621,6 +1675,13 @@ abstract class BaseEditorActivity :
16211675
return super.dispatchTouchEvent(ev)
16221676
}
16231677

1678+
private fun isTouchOnBottomSheetTabs(ev: MotionEvent): Boolean {
1679+
val tabs = contentOrNull?.bottomSheet?.binding?.tabs ?: return false
1680+
val rect = Rect()
1681+
if (!tabs.getGlobalVisibleRect(rect)) return false
1682+
return rect.contains(ev.rawX.toInt(), ev.rawY.toInt())
1683+
}
1684+
16241685
private fun showTooltip(tag: String) {
16251686
TooltipManager.showIdeCategoryTooltip(
16261687
context = this,

‎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.plugins.manager.ui.PluginUiActionManager
@@ -91,6 +96,7 @@ import com.itsaky.androidide.utils.EditorSidebarActions
9196
import com.itsaky.androidide.utils.IntentUtils.openImage
9297
import com.itsaky.androidide.utils.UniqueNameBuilder
9398
import com.itsaky.androidide.utils.flashSuccess
99+
import com.itsaky.androidide.utils.forEachViewRecursively
94100
import kotlinx.coroutines.Dispatchers
95101
import kotlinx.coroutines.NonCancellable
96102
import kotlinx.coroutines.launch
@@ -99,6 +105,7 @@ import org.adfa.constants.CONTENT_KEY
99105
import org.greenrobot.eventbus.Subscribe
100106
import org.greenrobot.eventbus.ThreadMode
101107
import java.io.File
108+
import java.util.WeakHashMap
102109
import java.util.concurrent.ConcurrentHashMap
103110
import java.util.concurrent.CopyOnWriteArrayList
104111
import java.util.concurrent.atomic.AtomicBoolean
@@ -126,6 +133,22 @@ open class EditorHandlerActivity :
126133

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

131154
private var pluginEditorProvider: EditorProviderImpl? = null
@@ -195,6 +218,8 @@ open class EditorHandlerActivity :
195218
mBuildEventListener.setActivity(this)
196219
super.onCreate(savedInstanceState)
197220

221+
supportFragmentManager.registerFragmentLifecycleCallbacks(pluginFontScalingListener, true)
222+
198223
editorViewModel._displayedFile.observe(
199224
this,
200225
) { fileIndex ->
@@ -393,25 +418,38 @@ open class EditorHandlerActivity :
393418
}
394419

395420
restoreOpenedPluginTabs()
421+
syncPluginUiFontSize()
396422
}
397423

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

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

406-
tabIds.forEach { tabId ->
407-
if (!pluginTabIndices.containsKey(tabId)) {
408-
selectPluginTabById(tabId)
442+
// Tab selection touches UI state, so keep it on the main thread.
443+
tabIds.forEach { tabId ->
444+
if (!pluginTabIndices.containsKey(tabId)) {
445+
selectPluginTabById(tabId)
446+
}
409447
}
410-
}
411448

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

@@ -1122,6 +1160,49 @@ open class EditorHandlerActivity :
11221160
showPluginCrashDialog(event)
11231161
}
11241162

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

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import androidx.core.net.toUri
1616
import androidx.fragment.app.Fragment
1717
import androidx.fragment.app.viewModels
1818
import androidx.navigation.fragment.findNavController
19+
import com.google.android.material.bottomsheet.BottomSheetBehavior
1920
import com.google.android.material.textfield.TextInputEditText
2021
import com.google.android.material.textfield.TextInputLayout
2122
import com.google.android.material.materialswitch.MaterialSwitch
2223
import com.itsaky.androidide.R
24+
import com.itsaky.androidide.activities.editor.BaseEditorActivity
2325
import com.itsaky.androidide.agent.repository.AiBackend
2426
import com.itsaky.androidide.agent.repository.Util.getCurrentBackend
2527
import com.itsaky.androidide.agent.viewmodel.AiSettingsViewModel
@@ -28,6 +30,7 @@ import com.itsaky.androidide.agent.viewmodel.ModelLoadingState
2830
import com.itsaky.androidide.databinding.FragmentAiSettingsBinding
2931
import com.itsaky.androidide.utils.flashInfo
3032
import com.itsaky.androidide.utils.getFileName
33+
import com.itsaky.androidide.viewmodel.BottomSheetViewModel
3134
import java.text.SimpleDateFormat
3235
import java.util.Date
3336
import java.util.Locale
@@ -50,6 +53,13 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) {
5053
val uriString = it.toString()
5154
viewModel.loadModelFromUri(uriString, requireContext())
5255
flashInfo("Attempting to load selected model...")
56+
57+
view?.postDelayed({
58+
(activity as? BaseEditorActivity)?.bottomSheetViewModel?.setSheetState(
59+
sheetState = BottomSheetBehavior.STATE_EXPANDED,
60+
currentTab = BottomSheetViewModel.TAB_AGENT
61+
)
62+
}, 100)
5363
}
5464
}
5565

@@ -111,7 +121,7 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) {
111121
val browseButton = view.findViewById<Button>(R.id.btn_browse_model)
112122
val loadSavedButton = view.findViewById<Button>(R.id.loadSavedButton)
113123
val modelStatusTextView = view.findViewById<TextView>(R.id.model_status_text_view)
114-
val engineStatusTextView = view.findViewById<TextView>(R.id.engine_status_text) // <-- NEW: Get reference to the new TextView
124+
val engineStatusTextView = view.findViewById<TextView>(R.id.engine_status_text)
115125
val simplePromptSwitch = view.findViewById<MaterialSwitch>(R.id.switch_simple_local_prompt)
116126
val shaInput = view.findViewById<TextInputEditText>(R.id.local_model_sha_input)
117127

@@ -204,6 +214,13 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) {
204214
}
205215
if (hasPermission) {
206216
viewModel.loadModelFromUri(savedUri, requireContext())
217+
218+
view?.postDelayed({
219+
(activity as? BaseEditorActivity)?.bottomSheetViewModel?.setSheetState(
220+
sheetState = BottomSheetBehavior.STATE_EXPANDED,
221+
currentTab = BottomSheetViewModel.TAB_AGENT
222+
)
223+
}, 100)
207224
} else {
208225
requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
209226
remove(SAVED_MODEL_URI_KEY)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.itsaky.androidide.agent.model
2+
3+
sealed interface ModelLoadResult {
4+
data class Loaded(
5+
val modelName: String
6+
) : ModelLoadResult
7+
8+
data class Rejected(
9+
val message: String
10+
) : ModelLoadResult
11+
12+
data class Failed(
13+
val message: String,
14+
val cause: Throwable? = null
15+
) : ModelLoadResult
16+
}

0 commit comments

Comments
 (0)