feat: add basic support for handling configuration changes

This commit is contained in:
Akash Yadav
2022-12-21 14:56:31 +05:30
parent 56a52d4b39
commit 9235885c68
17 changed files with 275 additions and 176 deletions
@@ -31,6 +31,7 @@ import com.itsaky.androidide.actions.locations.CodeActionsMenu
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.resolveAttr
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
/**
* Default implementation for the [ActionsRegistry]
@@ -40,7 +41,7 @@ import java.util.concurrent.CompletableFuture
class DefaultActionsRegistry : ActionsRegistry() {
private val log = ILogger.newInstance("DefaultActionsRegistry")
private val actions: MutableMap<String, MutableMap<String, ActionItem>> = mutableMapOf()
private val actions = ConcurrentHashMap<String, HashMap<String, ActionItem>>()
private val listeners = mutableSetOf<ActionExecListener>()
init {
@@ -49,7 +50,7 @@ class DefaultActionsRegistry : ActionsRegistry() {
override fun getActions(location: ActionItem.Location): MutableMap<String, ActionItem> {
if (actions[location.id] == null) {
actions[location.id] = mutableMapOf()
actions[location.id] = hashMapOf()
}
return actions[location.id]!!
+4 -4
View File
@@ -36,7 +36,7 @@
</activity>
<activity
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|uiMode"
android:configChanges="orientation|screenSize"
android:exported="true"
android:theme="@style/AppTheme.Splash">
<intent-filter>
@@ -47,17 +47,17 @@
<activity
android:name=".activities.editor.EditorActivityKt"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|uiMode"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.PreferencesActivity"
android:configChanges="orientation|screenSize" />
<activity
android:name=".activities.AboutActivity"
android:configChanges="orientation|screenSize|uiMode" />
android:configChanges="orientation|screenSize" />
<activity
android:name=".activities.TerminalActivity"
android:configChanges="orientation|screenSize|uiMode"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustResize" />
<service
@@ -42,7 +42,7 @@ class FileTreeAction() : EditorActivityAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.binding?.root?.apply {
context.binding.root.apply {
if (!isDrawerOpen(GravityCompat.END)) {
openDrawer(GravityCompat.END)
return true
@@ -38,7 +38,7 @@ class CloseFileAction(context: Context) : FileTabAction() {
}
override fun EditorHandlerActivity.doAction(data: ActionData): Boolean {
binding?.tabs?.selectedTabPosition?.let { closeFile(it) }
binding.tabs.selectedTabPosition.let { closeFile(it) }
return true
}
}
@@ -84,7 +84,6 @@ import com.itsaky.androidide.ui.editor.CodeEditorView
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.utils.ActionMenuUtils.createMenu
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
import com.itsaky.androidide.utils.EditorActivityActions.Companion.register as registerActivityActions
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
import com.itsaky.androidide.utils.SingleSessionCallback
@@ -95,11 +94,12 @@ import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
import com.itsaky.androidide.xml.widgets.WidgetTableRegistry
import com.itsaky.toaster.Toaster.Type.ERROR
import com.itsaky.toaster.toast
import java.io.File
import java.util.Objects
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
import java.io.File
import java.util.Objects
import com.itsaky.androidide.utils.EditorActivityActions.Companion.register as registerActivityActions
/**
* Base class for EditorActivity which handles most of the view related things.
@@ -119,18 +119,20 @@ abstract class BaseEditorActivity :
protected var filesTreeFragment: FileTreeFragment? = null
protected var editorBottomSheet: BottomSheetBehavior<out View?>? = null
protected var isDestroying = false
protected var isConfigChange = false
protected var wasInitializing = false
protected val log: ILogger = ILogger.newInstance("EditorActivity")
protected val logReceiver: LogReceiver = LogReceiver().setLogListener(::appendApkLog)
var uiDesignerResultLauncher: ActivityResultLauncher<Intent>? = null
val viewModel by viewModels<EditorViewModel>()
var binding: ActivityEditorBinding? = null
lateinit var binding: ActivityEditorBinding
protected set
private val onBackPressedCallback: OnBackPressedCallback =
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val binding = this@BaseEditorActivity.binding ?: return
if (binding.root.isDrawerOpen(GravityCompat.END)) {
binding.root.closeDrawer(GravityCompat.END)
} else if (binding.root.isDrawerOpen(GravityCompat.START)) {
@@ -159,6 +161,7 @@ abstract class BaseEditorActivity :
protected abstract fun doConfirmProjectClose()
protected open fun preDestroy() {
viewModel.isConfigChange = !isDestroying
try {
unregisterReceiver(logReceiver)
} catch (th: Throwable) {
@@ -167,28 +170,27 @@ abstract class BaseEditorActivity :
}
protected open fun postDestroy() {
Lookup.DEFAULT.unregisterAll()
ApiVersionsRegistry.getInstance().clear()
ResourceTableRegistry.getInstance().clear()
WidgetTableRegistry.getInstance().clear()
binding = null
binding = null
if (isDestroying) {
Lookup.DEFAULT.unregisterAll()
ApiVersionsRegistry.getInstance().clear()
ResourceTableRegistry.getInstance().clear()
WidgetTableRegistry.getInstance().clear()
}
}
override fun bindLayout(): View {
this.binding = ActivityEditorBinding.inflate(layoutInflater)
this.diagnosticInfoBinding = this.binding!!.diagnosticInfo
return this.binding!!.root
this.diagnosticInfoBinding = this.binding.diagnosticInfo
return this.binding.root
}
@Subscribe(threadMode = MAIN)
open fun onInstallationResult(event: InstallationResultEvent) {
val intent = event.intent
if (binding == null || isDestroying) {
if (isDestroying) {
return
}
val binding = this.binding ?: return
val packageName = onResult(this, intent)
if (packageName != null) {
Snackbar.make(binding.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG)
@@ -209,15 +211,20 @@ abstract class BaseEditorActivity :
projectPath = savedInstanceState.getString(KEY_PROJECT_PATH)!!
}
this.wasInitializing = viewModel.isInitializing
this.isConfigChange = viewModel.isConfigChange
viewModel.isConfigChange = false
viewModel.isInitializing = false
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
lifecycle.addObserver(mLifecycleObserver)
setSupportActionBar(binding?.editorToolbar)
setSupportActionBar(binding.editorToolbar)
dmonStatusFragment = getDaemonStatusFragment()
setupDrawerToggle()
binding?.tabs?.addOnTabSelectedListener(this)
binding.tabs.addOnTabSelectedListener(this)
setupViews()
@@ -289,7 +296,7 @@ abstract class BaseEditorActivity :
id.editornav_terminal -> openTerminal()
}
binding?.root?.closeDrawer(GravityCompat.START)
binding.root.closeDrawer(GravityCompat.START)
return false
}
@@ -324,7 +331,7 @@ abstract class BaseEditorActivity :
}
open fun appendApkLog(line: LogLine) {
binding?.bottomSheet?.appendApkLog(line)
binding.bottomSheet.appendApkLog(line)
}
open fun handleSearchResults(map: Map<File, List<SearchResult>>?) {
@@ -347,11 +354,11 @@ abstract class BaseEditorActivity :
}
open fun setSearchResultAdapter(adapter: SearchListAdapter) {
binding?.bottomSheet?.setSearchResultAdapter(adapter)
binding.bottomSheet.setSearchResultAdapter(adapter)
}
open fun setDiagnosticsAdapter(adapter: DiagnosticsAdapter) {
binding?.bottomSheet?.setDiagnosticsAdapter(adapter)
binding.bottomSheet.setDiagnosticsAdapter(adapter)
}
open fun showDaemonStatus() {
@@ -387,23 +394,19 @@ abstract class BaseEditorActivity :
}
val index =
binding
?.bottomSheet
?.pagerAdapter
?.findIndexOfFragmentByClass(SearchResultFragment::class.java)
?: -1
binding.bottomSheet.pagerAdapter.findIndexOfFragmentByClass(SearchResultFragment::class.java)
if (index >= 0 && index < (binding?.bottomSheet?.binding?.tabs?.tabCount ?: 0)) {
binding?.bottomSheet?.binding?.tabs?.getTabAt(index)?.select()
if (index >= 0 && index < binding.bottomSheet.binding.tabs.tabCount) {
binding.bottomSheet.binding.tabs.getTabAt(index)?.select()
}
}
open fun handleDiagnosticsResultVisibility(errorVisible: Boolean) {
binding?.bottomSheet?.handleDiagnosticsResultVisibility(errorVisible)
binding.bottomSheet.handleDiagnosticsResultVisibility(errorVisible)
}
open fun handleSearchResultVisibility(errorVisible: Boolean) {
binding?.bottomSheet?.handleSearchResultVisibility(errorVisible)
binding.bottomSheet.handleSearchResultVisibility(errorVisible)
}
open fun showFirstBuildNotice() {
@@ -461,7 +464,6 @@ abstract class BaseEditorActivity :
}
private fun setupDrawerToggle() {
val binding = this.binding ?: return
val toggle =
ActionBarDrawerToggle(
this,
@@ -479,23 +481,17 @@ abstract class BaseEditorActivity :
private fun onBuildStatusChanged() {
val visible = viewModel.isBuildInProgress || viewModel.isInitializing
binding?.buildProgressIndicator?.visibility = if (visible) View.VISIBLE else View.GONE
binding.buildProgressIndicator.visibility = if (visible) View.VISIBLE else View.GONE
invalidateOptionsMenu()
}
private fun setupViews() {
viewModel._isBuildInProgress.observe(this) { onBuildStatusChanged() }
viewModel._isInitializing.observe(this) { onBuildStatusChanged() }
viewModel._statusText.observe(this) {
binding?.bottomSheet?.setStatus(it.first, it.second)
}
viewModel._statusText.observe(this) { binding.bottomSheet.setStatus(it.first, it.second) }
viewModel.observeFiles(this) { files ->
if (binding == null) {
return@observeFiles
}
binding?.apply {
binding.apply {
if (files == null || files.isEmpty()) {
tabs.visibility = View.GONE
viewContainer.displayedChild = 1
@@ -525,11 +521,11 @@ abstract class BaseEditorActivity :
}
private fun setupNoEditorView() {
binding?.noEditorSummary?.movementMethod = LinkMovementMethod()
binding.noEditorSummary.movementMethod = LinkMovementMethod()
val filesSpan: ClickableSpan =
object : ClickableSpan() {
override fun onClick(widget: View) {
binding?.root?.openDrawer(GravityCompat.END)
binding.root.openDrawer(GravityCompat.END)
}
}
val bottomSheetSpan: ClickableSpan =
@@ -541,7 +537,7 @@ abstract class BaseEditorActivity :
val sb = SpannableStringBuilder()
appendClickableSpan(sb, string.msg_swipe_for_files, filesSpan)
appendClickableSpan(sb, string.msg_swipe_for_output, bottomSheetSpan)
binding?.noEditorSummary?.text = sb
binding.noEditorSummary.text = sb
}
private fun appendClickableSpan(
@@ -564,7 +560,7 @@ abstract class BaseEditorActivity :
}
private fun setupBottomSheet() {
editorBottomSheet = BottomSheetBehavior.from<View>(binding!!.bottomSheet)
editorBottomSheet = BottomSheetBehavior.from<View>(binding.bottomSheet)
editorBottomSheet?.addBottomSheetCallback(
object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
@@ -577,7 +573,7 @@ abstract class BaseEditorActivity :
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
binding?.apply {
binding.apply {
val editorScale = 1 - slideOffset * (1 - EDITOR_CONTAINER_SCALE_FACTOR)
this.bottomSheet.onSlide(slideOffset)
this.viewContainer.scaleX = editorScale
@@ -590,7 +586,7 @@ abstract class BaseEditorActivity :
val observer: OnGlobalLayoutListener =
object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding?.let {
binding.let {
it.viewContainer.pivotY = 0f
it.viewContainer.pivotX = it.viewContainer.width / 2f
it.viewContainer.viewTreeObserver.removeOnGlobalLayoutListener(this)
@@ -598,14 +594,14 @@ abstract class BaseEditorActivity :
}
}
binding?.apply {
binding.apply {
viewContainer.viewTreeObserver.addOnGlobalLayoutListener(observer)
bottomSheet.setOffsetAnchor(editorToolbar)
}
}
private fun refreshSymbolInput(editor: CodeEditorView) {
binding?.bottomSheet?.refreshSymbolInput(editor)
binding.bottomSheet.refreshSymbolInput(editor)
}
private fun setupDiagnosticInfo() {
@@ -625,7 +621,7 @@ abstract class BaseEditorActivity :
private fun onSoftInputChanged() {
invalidateOptionsMenu()
binding?.bottomSheet?.onSoftInputChanged()
binding.bottomSheet.onSoftInputChanged()
}
private fun openTerminal() {
@@ -652,7 +648,7 @@ abstract class BaseEditorActivity :
}
private fun getSyncBanner(): MaterialBanner? {
return binding?.run {
return binding.run {
return@run syncBanner
.setContentTextColor(resolveAttr(attr.colorOnPrimaryContainer))
.setBannerBackgroundColor(resolveAttr(attr.colorPrimaryContainer))
@@ -666,7 +662,7 @@ abstract class BaseEditorActivity :
return object : SingleSessionCallback() {
override fun onCreated(sessionId: Int) {
log.debug("on session created:", sessionId)
binding?.apply {
binding.apply {
bottomSheet.setActionText(getString(string.msg_installing_apk))
bottomSheet.setActionProgress(0)
bottomSheet.showChild(EditorBottomSheet.CHILD_ACTION)
@@ -674,11 +670,11 @@ abstract class BaseEditorActivity :
}
override fun onProgressChanged(sessionId: Int, progress: Float) {
binding?.bottomSheet?.setActionProgress((progress * 100f).toInt())
binding.bottomSheet.setActionProgress((progress * 100f).toInt())
}
override fun onFinished(sessionId: Int, success: Boolean) {
binding?.apply {
binding.apply {
bottomSheet.showChild(EditorBottomSheet.CHILD_HEADER)
bottomSheet.setActionProgress(0)
if (!success) {
@@ -51,43 +51,47 @@ import java.io.File
* @author Akash Yadav
*/
open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
override fun doOpenFile(file: File, selection: Range?) {
openFileAndSelect(file, selection)
}
override fun doCloseAll(runAfter: () -> Unit) {
closeAll(runAfter)
}
override fun doSaveAll(): Boolean {
return saveAll()
}
override fun provideCurrentEditor(): CodeEditorView? {
return getCurrentEditor()
}
override fun provideEditorAt(index: Int): CodeEditorView? {
return getEditorAtIndex(index)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel._displayedFile.observe(this) { this.binding?.editorContainer?.displayedChild = it }
viewModel._displayedFile.observe(this) { this.binding.editorContainer.displayedChild = it }
viewModel._fileTreeDrawerOpened.observe(this) { opened ->
this.binding?.editorDrawerLayout?.apply {
this.binding.editorDrawerLayout.apply {
if (opened) openDrawer(GravityCompat.END) else closeDrawer(GravityCompat.END)
}
}
if (viewModel.getOpenedFileCount() != binding.tabs.tabCount) {
// The activity proabably has been restarted after a configuration change
// restore the opened files
val files = viewModel.getOpenedFiles()
viewModel.removeAllFiles()
files.forEach(this::openFile)
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
@SuppressLint("RestrictedApi")
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
if (menu is MenuBuilder) {
@@ -95,7 +99,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ensureToolbarMenu(menu)
return true
@@ -108,7 +112,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
override fun getEditorAtIndex(index: Int): CodeEditorView? {
return binding?.editorContainer?.getChildAt(index) as CodeEditorView?
return binding.editorContainer.getChildAt(index) as CodeEditorView?
}
override fun openFileAndSelect(file: File, selection: Range?) {
@@ -134,7 +138,6 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
return null
}
val binding = this.binding ?: return null
val index = openFileAndGetIndex(file, range)
val tab = binding.tabs.getTabAt(index)
if (tab != null && index >= 0 && !tab.isSelected) {
@@ -154,7 +157,6 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
override fun openFileAndGetIndex(file: File, selection: Range?): Int {
val binding = this.binding ?: return -1
val openedFileIndex = findIndexOfEditorByFile(file)
if (openedFileIndex != -1) {
log.error("File is already opened. File: $file")
@@ -181,7 +183,6 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
override fun getEditorForFile(file: File): CodeEditorView? {
val binding = this.binding ?: return null
for (i in 0 until viewModel.getOpenedFileCount()) {
val editor = binding.editorContainer.getChildAt(i) as CodeEditorView
if (file == editor.file) {
@@ -302,7 +303,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
viewModel.removeFile(index)
binding?.apply {
binding.apply {
tabs.removeTabAt(index)
editorContainer.removeViewAt(index)
}
@@ -311,7 +312,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
return
}
binding?.tabs?.requestLayout()
binding.tabs.requestLayout()
}
override fun closeAll() {
@@ -341,7 +342,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
notifyFilesUnsaved(unsavedFiles) { closeOthers() }
}
}
open fun ensureToolbarMenu(menu: Menu) {
menu.clear()
val data = ActionData()
@@ -377,7 +378,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
viewModel.removeAllFiles()
binding?.apply {
binding.apply {
tabs.removeAllTabs()
tabs.requestLayout()
editorContainer.removeAllViews()
@@ -33,14 +33,16 @@ import com.itsaky.androidide.databinding.LayoutSearchProjectBinding
import com.itsaky.androidide.fragments.sheets.ProgressSheet
import com.itsaky.androidide.handlers.EditorBuildEventListener
import com.itsaky.androidide.handlers.LspHandler.connectClient
import com.itsaky.androidide.handlers.LspHandler.destroy
import com.itsaky.androidide.handlers.LspHandler.destroyLanguageServers
import com.itsaky.androidide.interfaces.IProjectHandler
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.IDELanguageClientImpl
import com.itsaky.androidide.preferences.internal.NO_OPENED_PROJECT
import com.itsaky.androidide.preferences.internal.lastOpenedProject
import com.itsaky.androidide.projects.ProjectManager.cachedInitResult
import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath
import com.itsaky.androidide.projects.ProjectManager.notifyProjectUpdate
import com.itsaky.androidide.projects.ProjectManager.projectInitialized
import com.itsaky.androidide.projects.ProjectManager.projectPath
import com.itsaky.androidide.projects.ProjectManager.rootProject
import com.itsaky.androidide.projects.ProjectManager.setupProject
@@ -54,16 +56,17 @@ import com.itsaky.androidide.utils.RecursiveFileSearcher
import com.itsaky.toaster.Toaster.Type.ERROR
import com.itsaky.toaster.toast
import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.regex.Pattern
import java.util.stream.Collectors
/** @author Akash Yadav */
@Suppress("MemberVisibilityCanBePrivate")
abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
protected var mSearchingProgress: ProgressSheet? = null
protected var mFindInProjectDialog: AlertDialog? = null
val findInProjectDialog: AlertDialog
get() {
if (mFindInProjectDialog == null) {
@@ -71,47 +74,57 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
}
return mFindInProjectDialog!!
}
protected val mBuildEventListener = EditorBuildEventListener()
private val mGradleServiceConnection: ServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val buildService = (service as GradleServiceBinder).service
log.info("Gradle build service has been started...")
Lookup.DEFAULT.update(BuildService.KEY_BUILD_SERVICE, buildService)
buildService.setEventListener(mBuildEventListener).startToolingServer {
initializeProject()
}
onGradleBuildServiceConnected(buildService)
}
override fun onServiceDisconnected(name: ComponentName?) {
log.info("Disconnected from Gradle build service...")
log.info("Disconnected from Gradle build service")
}
}
abstract fun doCloseAll(runAfter: () -> Unit)
override fun doDismissSearchProgress() {
if (mSearchingProgress?.isShowing == true) {
mSearchingProgress!!.dismiss()
}
}
override fun doConfirmProjectClose() {
confirmProjectClose()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBuildEventListener.setActivity(this as EditorHandlerActivity)
startServices()
}
override fun preDestroy() {
closeProject(false)
if (isDestroying) {
closeProject(false)
projectInitialized = false
cachedInitResult = null
}
super.preDestroy()
unbindService(mGradleServiceConnection)
if (isDestroying) {
try {
app.unbindService(mGradleServiceConnection)
} catch (err: Throwable) {
log.error("Unable to unbind service", err)
} finally {
Lookup.DEFAULT.unregister(BuildService.KEY_BUILD_SERVICE)
}
}
}
override fun setStatus(status: CharSequence, @GravityInt gravity: Int) {
@@ -119,7 +132,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
}
override fun appendBuildOutput(str: String) {
binding?.bottomSheet?.appendBuildOut(str)
binding.bottomSheet.appendBuildOut(str)
}
override fun notifySyncNeeded() {
@@ -127,11 +140,19 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
}
override fun startServices() {
val service = Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE) as GradleBuildService?
if (service != null) {
log.info("Reusing already started Gradle build service")
onGradleBuildServiceConnected(service)
return
}
if (
bindService(
app.bindService(
Intent(this, GradleBuildService::class.java),
mGradleServiceConnection,
BIND_AUTO_CREATE
BIND_AUTO_CREATE or BIND_IMPORTANT
)
) {
log.info("Bind request for Gradle build service was successful...")
@@ -143,13 +164,22 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
}
override fun initializeProject() {
val projectPath = projectPath
val projectDir = File(projectPath)
if (!projectDir.exists()) {
log.error("Project directory does not exist. Cannot initialize project")
return
}
val initialized = projectInitialized && cachedInitResult != null
log.debug(projectInitialized, cachedInitResult, wasInitializing)
// When returning after a configuration change between the initialization process,
// we do not want to start another project initialization
if (initialized && !wasInitializing) {
return
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread { preProjectInit() }
@@ -158,16 +188,27 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
log.error("No build service found. Cannot initialize project.")
return
}
val future =
if (!(isConfigChange && wasInitializing) && !initialized) {
buildService.initializeProject(projectDir.absolutePath)
} else {
// The project initialization was in progress before the configuration change
// In this case, we should not start another project initialization
CompletableFuture.supplyAsync {
log.warn("Project has already been initialized. Skipping initialization process.")
cachedInitResult
}
}
val future = buildService.initializeProject(projectDir.absolutePath)
future.whenCompleteAsync { result: InitializeResult?, error: Throwable? ->
future.whenCompleteAsync { result, error ->
if (result == null || error != null) {
log.error("An error occurred initializing the project with Tooling API", error)
setStatus(getString(string.msg_project_initialization_failed))
return@whenCompleteAsync
}
onProjectInitialized()
onProjectInitialized(result)
}
}
@@ -176,86 +217,100 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
if (IDELanguageClientImpl.isInitialized()) {
IDELanguageClientImpl.shutdown()
}
destroy(isChangingConfigurations)
destroyLanguageServers(isChangingConfigurations)
} catch (err: Throwable) {
log.error("Unable to stop editor services. Please report this issue.", err)
}
}
protected open fun onProjectInitialized() {
protected fun onGradleBuildServiceConnected(service: GradleBuildService) {
log.info("Connected to Gradle build service")
service.setEventListener(mBuildEventListener)
if (!service.isToolingServerStarted) {
service.startToolingServer { initializeProject() }
} else {
initializeProject()
}
}
protected open fun onProjectInitialized(result: InitializeResult) {
if (isConfigChange && projectInitialized && result == cachedInitResult) {
return
}
cachedInitResult = result
setupProject()
notifyProjectUpdate()
ThreadUtils.runOnUiThread { postProjectInit() }
}
protected open fun postProjectInit() {
this.binding ?: return
initialSetup()
setStatus(getString(string.msg_project_initialized))
viewModel.isInitializing = false
projectInitialized = true
if (mFindInProjectDialog?.isShowing == true) {
mFindInProjectDialog!!.dismiss()
}
mFindInProjectDialog = null // Create the dialog again if needed
}
protected open fun createFindInProjectDialog(): AlertDialog? {
if (rootProject == null) {
log.warn("No root project model found. Is the project initialized?")
toast(getString(string.msg_project_not_initialized), ERROR)
return null
}
val moduleDirs = try {
rootProject!!.subModules.stream()
.map(Project::projectDir)
.collect(Collectors.toList())
} catch (e: Throwable) {
toast(getString(string.msg_no_modules), ERROR)
emptyList()
}
val moduleDirs =
try {
rootProject!!.subModules.stream().map(Project::projectDir).collect(Collectors.toList())
} catch (e: Throwable) {
toast(getString(string.msg_no_modules), ERROR)
emptyList()
}
return createFindInProjectDialog(moduleDirs)
}
protected open fun createFindInProjectDialog(moduleDirs: List<File>): AlertDialog? {
val srcDirs = mutableListOf<File>()
val binding = LayoutSearchProjectBinding.inflate(layoutInflater)
binding.modulesContainer.removeAllViews()
for (i in moduleDirs.indices) {
val module = moduleDirs[i]
val src = File(module, "src")
if (!module.exists() || !module.isDirectory || !src.exists() || !src.isDirectory) {
continue
}
val check = CheckBox(this)
check.text = module.name
check.isChecked = true
val params = MarginLayoutParams(-2, -2)
params.bottomMargin = SizeUtils.dp2px(4f)
binding.modulesContainer.addView(check, params)
srcDirs.add(src)
}
val builder = newMaterialDialogBuilder(this)
builder.setTitle(string.menu_find_project)
builder.setView(binding.root)
builder.setCancelable(false)
builder.setPositiveButton(
string.menu_find
) { dialog, _ ->
builder.setPositiveButton(string.menu_find) { dialog, _ ->
val text = binding.input.editText!!.text.toString().trim()
if (text.isEmpty()) {
toast(string.msg_empty_search_query, ERROR)
return@setPositiveButton
}
val searchDirs = mutableListOf<File>()
for (i in 0 until binding.modulesContainer.childCount) {
val check = binding.modulesContainer.getChildAt(i) as CheckBox
@@ -263,12 +318,16 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
searchDirs.add(srcDirs[i])
}
}
val extensions = binding.filter.editText!!.text.toString().trim()
val extensionList = mutableListOf<String>()
if (extensions.isNotEmpty()) {
if (extensions.contains("|")) {
for (str in extensions.split(Pattern.quote("|").toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
for (str in
extensions
.split(Pattern.quote("|").toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()) {
if (str.trim().isEmpty()) {
continue
}
@@ -278,26 +337,27 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
extensionList.add(extensions)
}
}
if (searchDirs.isEmpty()) {
toast(string.msg_select_search_modules, ERROR)
} else {
dialog.dismiss()
getProgressSheet(string.msg_searching_project)?.apply {
show(supportFragmentManager, "search_in_project_progress")
}
RecursiveFileSearcher.searchRecursiveAsync(
text, extensionList, searchDirs) { results -> handleSearchResults(results) }
RecursiveFileSearcher.searchRecursiveAsync(text, extensionList, searchDirs) { results ->
handleSearchResults(results)
}
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
mFindInProjectDialog = builder.create()
return mFindInProjectDialog
}
private fun initialSetup() {
lastOpenedProject = getProjectDirPath()
try {
@@ -306,21 +366,21 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
log.warn("Project not initialized. Skipping initial setup...")
return
}
var projectName = rootProject.name
if (projectName.isEmpty()) {
projectName = File(getProjectDirPath()).name
}
supportActionBar!!.subtitle = projectName
} catch (th: Throwable) {
// ignored
}
}
private fun closeProject(manualFinish: Boolean) {
stopServices()
// Make sure we close files
// This will make sure that file contents are not erased.
doCloseAll {
@@ -330,42 +390,41 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
}
}
}
private fun confirmProjectClose() {
val builder = newMaterialDialogBuilder(this)
builder.setTitle(string.title_confirm_project_close)
builder.setMessage(string.msg_confirm_project_close)
builder.setNegativeButton(string.no, null)
builder.setPositiveButton(
string.yes
) { dialog, _ ->
builder.setPositiveButton(string.yes) { dialog, _ ->
dialog.dismiss()
closeProject(true)
}
builder.show()
}
private fun initLspClient() {
if (!IDELanguageClientImpl.isInitialized()) {
IDELanguageClientImpl.initialize(this as EditorHandlerActivity)
}
connectClient(IDELanguageClientImpl.getInstance())
}
private fun preProjectInit() {
setStatus(getString(string.msg_initializing_project))
viewModel.isInitializing = true
}
open fun getProgressSheet(msg: Int): ProgressSheet? {
doDismissSearchProgress()
mSearchingProgress = ProgressSheet().apply {
isCancelable = false
setMessage(getString(msg))
setSubMessageEnabled(false)
}
mSearchingProgress =
ProgressSheet().apply {
isCancelable = false
setMessage(getString(msg))
setSubMessageEnabled(false)
}
return mSearchingProgress
}
}
@@ -38,7 +38,7 @@ public class IDEApplication extends BaseApplication {
private static final ILogger LOG = ILogger.newInstance("IDEApplication");
private static IDEApplication instance;
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
public static IDEApplication getInstance() {
return instance;
}
@@ -49,7 +49,7 @@ public class IDEApplication extends BaseApplication {
this.uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this::handleCrash);
super.onCreate();
EventBus.builder()
.addIndex(new AppEventsIndex())
.addIndex(new ProjectsApiEventsIndex())
@@ -37,6 +37,9 @@ class EditorBuildEventListener : GradleBuildService.EventListener {
fun setActivity(activity: EditorHandlerActivity) {
this.activityReference = WeakReference(activity)
}
fun releaes() {
}
override fun prepareBuild() {
val isFirstBuild = isFirstBuild
@@ -103,7 +103,7 @@ class FileTreeActionHandler : BaseEventHandler() {
}
val context = event[Context::class.java]!! as EditorHandlerActivity
context.binding?.root?.closeDrawer(GravityCompat.END)
context.binding.root.closeDrawer(GravityCompat.END)
if (event.file.name.endsWith(".apk")) {
ApkInstaller.installApk(
context,
@@ -39,7 +39,7 @@ object LspHandler {
ILanguageServerRegistry.getDefault().connectClient(client)
}
fun destroy(isConfigurationChange: Boolean) {
fun destroyLanguageServers(isConfigurationChange: Boolean) {
if (isConfigurationChange) {
return
}
@@ -37,6 +37,7 @@ import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -96,7 +97,7 @@ public class GradleBuildService extends Service implements BuildService, IToolin
private final IBinder mBinder = new GradleServiceBinder();
private boolean isToolingServerStarted = false;
private boolean isBuildInProgress = false;
private Thread toolingServerThread;
private ToolingServerRunner toolingServerThread;
private NotificationManager notificationManager;
private IToolingApiServer server;
private EventListener eventListener;
@@ -105,8 +106,14 @@ public class GradleBuildService extends Service implements BuildService, IToolin
public void onCreate() {
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
showNotification(getString(R.string.build_status_idle), false);
Lookup.DEFAULT.update(BuildService.KEY_BUILD_SERVICE, this);
}
public boolean isToolingServerStarted() {
return isToolingServerStarted;
}
private void showNotification(final String message, final boolean isProgress) {
LOG.info("Showing notification to user...");
startForeground(NOTIFICATION_ID, buildNotification(message, isProgress));
@@ -163,6 +170,7 @@ public class GradleBuildService extends Service implements BuildService, IToolin
@Nullable
@Override
public IBinder onBind(Intent intent) {
LOG.debug("onBind() called with: intent = [" + intent + "]");
return mBinder;
}
@@ -397,8 +405,15 @@ public class GradleBuildService extends Service implements BuildService, IToolin
public void startToolingServer(@Nullable OnServerStartListener listener) {
if (toolingServerThread == null || !toolingServerThread.isAlive()) {
toolingServerThread = new Thread(new ToolingServerRunner(listener));
toolingServerThread = new ToolingServerRunner(listener);
toolingServerThread.start();
return;
}
if (toolingServerThread.isStarted && listener != null) {
listener.onServerStarted();
} else {
toolingServerThread.setListener(listener);
}
}
@@ -465,14 +480,19 @@ public class GradleBuildService extends Service implements BuildService, IToolin
}
}
private class ToolingServerRunner implements Runnable {
private class ToolingServerRunner extends Thread {
@Nullable private final OnServerStartListener listener;
@Nullable private OnServerStartListener listener;
private boolean isStarted = false;
public ToolingServerRunner(@Nullable OnServerStartListener listener) {
this.listener = listener;
}
public void setListener(@Nullable final OnServerStartListener listener) {
this.listener = listener;
}
@Override
public void run() {
try {
@@ -525,6 +545,7 @@ public class GradleBuildService extends Service implements BuildService, IToolin
ProjectManager.INSTANCE.setupProject();
isStarted = true;
if (listener != null) {
listener.onServerStarted();
}
@@ -18,6 +18,9 @@ package com.itsaky.androidide.utils
import android.content.Context
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_FILE_TABS
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TEXT_ACTIONS
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.build.CancelBuildAction
import com.itsaky.androidide.actions.build.ProjectSyncAction
@@ -79,6 +82,10 @@ class EditorActivityActions {
}
@JvmStatic
fun clear() = ActionsRegistry.getInstance().clearActions(ActionItem.Location.EDITOR_TOOLBAR)
fun clear() {
val locations = arrayOf(EDITOR_TOOLBAR, EDITOR_FILE_TABS, EDITOR_TEXT_ACTIONS)
val registry = ActionsRegistry.getInstance()
locations.forEach(registry::clearActions)
}
}
}
@@ -30,6 +30,7 @@ class EditorViewModel : ViewModel() {
internal val _isBuildInProgress = MutableLiveData(false)
internal val _isInitializing = MutableLiveData(false)
internal val _isConfigurationChange = MutableLiveData(false)
internal val _statusText = MutableLiveData<Pair<CharSequence, @GravityInt Int>>("" to CENTER)
internal val _displayedFile = MutableLiveData(-1)
internal val _fileTreeDrawerOpened = MutableLiveData(false)
@@ -42,6 +43,12 @@ class EditorViewModel : ViewModel() {
*/
private val mCurrentFile = MutableLiveData<Pair<Int, File?>?>(null)
var isConfigChange: Boolean
get() = _isConfigurationChange.value ?: false
set(value) {
_isConfigurationChange.value = value
}
var isBuildInProgress: Boolean
get() = _isBuildInProgress.value ?: false
set(value) {
@@ -30,6 +30,7 @@ import com.itsaky.androidide.projects.api.Project
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.projects.util.ProjectTransformer
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.utils.ILogger
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -52,6 +53,9 @@ object ProjectManager : EventReceiver {
var rootProject: Project? = null
var app: AndroidModule? = null
var projectInitialized: Boolean = false
var cachedInitResult: InitializeResult? = null
@JvmOverloads
fun setupProject(project: IProject = Lookup.DEFAULT.lookup(BuildService.KEY_PROJECT_PROXY)!!) {
@@ -61,7 +65,6 @@ object ProjectManager : EventReceiver {
this.app = this.rootProject!!.findFirstAndroidAppModule()
this.rootProject!!.subModules.filterIsInstance(ModuleProject::class.java).forEach {
it.indexSourcesAndClasspaths()
if (it is AndroidModule) {
it.readResources()
}
Binary file not shown.
+1
View File
@@ -20,6 +20,7 @@
<application android:theme="@style/Theme.AndroidIDE">
<activity
android:name=".UIDesignerActivity"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ShowXmlActivity"