diff --git a/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt b/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt index eafb1acb5f..cf6c1bd0c5 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt @@ -19,10 +19,10 @@ package com.itsaky.androidide.actions.build import android.content.Context import androidx.core.content.ContextCompat -import com.itsaky.androidide.resources.R -import com.itsaky.androidide.resources.R.string import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.BaseBuildAction +import com.itsaky.androidide.resources.R +import com.itsaky.androidide.resources.R.string /** * Triggers a project sync request. @@ -40,6 +40,6 @@ class ProjectSyncAction(context: Context) : BaseBuildAction() { } override fun execAction(data: ActionData): Any { - return data.getActivity()!!.initializeProject() + return data.requireActivity().initializeProject() } } diff --git a/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt b/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt index 2831e7c3b7..364fc8a4d9 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt @@ -46,7 +46,7 @@ class PreviewLayoutAction(context: Context) : EditorRelatedAction() { override fun prepare(data: ActionData) { super.prepare(data) - val viewModel = data.getActivity()!!.viewModel + val viewModel = data.requireActivity().editorViewModel if (viewModel.isInitializing) { visible = true enabled = false @@ -61,12 +61,12 @@ class PreviewLayoutAction(context: Context) : EditorRelatedAction() { val file = editor.file!! val isXml = file.name.endsWith(".xml") - + if (!isXml) { markInvisible() return } - + val type = try { extractPathData(file).type } catch (err: Throwable) { diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/FileTabAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/FileTabAction.kt index 7a9b84b9a7..83d27cf624 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/FileTabAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/FileTabAction.kt @@ -48,7 +48,7 @@ abstract class FileTabAction : EditorActivityAction() { return } - visible = activity.viewModel.getOpenedFiles().isNotEmpty() + visible = activity.editorViewModel.getOpenedFiles().isNotEmpty() enabled = visible } diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt index e2b3b2c440..eb3aeffe04 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt @@ -50,7 +50,7 @@ class SaveFileAction(context: Context) : EditorRelatedAction() { return } - visible = context.viewModel.getOpenedFiles().isNotEmpty() + visible = context.editorViewModel.getOpenedFiles().isNotEmpty() enabled = context.areFilesModified() } @@ -81,7 +81,7 @@ class SaveFileAction(context: Context) : EditorRelatedAction() { } if (saveResult.gradleSaved) { - context.notifySyncNeeded() + context.editorViewModel.isSyncNeeded = true } context.invalidateOptionsMenu() diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index cdd7e7f59a..bdd74c94cd 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -46,8 +46,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCa import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.Tab -import com.itsaky.androidide.R.attr -import com.itsaky.androidide.R.drawable import com.itsaky.androidide.R.string import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_FILE_TABS import com.itsaky.androidide.adapters.DiagnosticsAdapter @@ -69,21 +67,15 @@ import com.itsaky.androidide.models.Range import com.itsaky.androidide.models.SearchResult import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath import com.itsaky.androidide.projects.ProjectManager.projectPath -import com.itsaky.androidide.projects.builder.BuildService import com.itsaky.androidide.ui.ContentTranslatingDrawerLayout 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.ApkInstallationSessionCallback -import com.itsaky.androidide.utils.DURATION_INDEFINITE import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.InstallationResultHandler.onResult import com.itsaky.androidide.utils.flashError -import com.itsaky.androidide.utils.flashbarBuilder -import com.itsaky.androidide.utils.resolveAttr -import com.itsaky.androidide.utils.showOnUiThread -import com.itsaky.androidide.utils.withIcon import com.itsaky.androidide.viewmodel.EditorViewModel import com.itsaky.androidide.xml.resources.ResourceTableRegistry import com.itsaky.androidide.xml.versions.ApiVersionsRegistry @@ -114,7 +106,8 @@ abstract class BaseEditorActivity : internal var installationCallback: ApkInstallationSessionCallback? = null var uiDesignerResultLauncher: ActivityResultLauncher? = null - val viewModel by viewModels() + val editorViewModel by viewModels() + lateinit var binding: ActivityEditorBinding protected set @@ -252,12 +245,12 @@ abstract class BaseEditorActivity : override fun onTabSelected(tab: Tab) { val position = tab.position - viewModel.displayedFileIndex = position + editorViewModel.displayedFileIndex = position val editorView = provideEditorAt(position)!! editorView.onEditorSelected() - viewModel.setCurrentFile(position, editorView.file) + editorViewModel.setCurrentFile(position, editorView.file) refreshSymbolInput(editorView) invalidateOptionsMenu() } @@ -344,27 +337,6 @@ abstract class BaseEditorActivity : .show() } - fun notifySyncNeeded(onConfirm: () -> Unit) { - val buildService = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) - if (buildService == null || buildService.isBuildInProgress) return - - flashbarBuilder( - duration = DURATION_INDEFINITE, - backgroundColor = resolveAttr(attr.colorSecondaryContainer), - messageColor = resolveAttr(attr.colorOnSecondaryContainer) - ) - .withIcon(drawable.ic_sync, colorFilter = resolveAttr(attr.colorOnSecondaryContainer)) - .message(string.msg_sync_needed) - .positiveActionText(string.btn_sync) - .positiveActionTapListener { - onConfirm() - it.dismiss() - } - .negativeActionText(string.btn_ignore_changes) - .negativeActionTapListener { it.dismiss() } - .showOnUiThread() - } - open fun getFileTreeFragment(): FileTreeFragment? { if (filesTreeFragment == null) { filesTreeFragment = @@ -374,8 +346,8 @@ abstract class BaseEditorActivity : } fun doSetStatus(text: CharSequence, @GravityInt gravity: Int) { - viewModel.statusText = text - viewModel.statusGravity = gravity + editorViewModel.statusText = text + editorViewModel.statusGravity = gravity } private fun tryLaunchApp(packageName: String) { @@ -440,18 +412,18 @@ abstract class BaseEditorActivity : private fun onBuildStatusChanged() { log.debug( - "onBuildStatusChanged: isInitializing: ${viewModel.isInitializing}, isBuildInProgress: ${viewModel.isBuildInProgress}") - val visible = viewModel.isBuildInProgress || viewModel.isInitializing + "onBuildStatusChanged: isInitializing: ${editorViewModel.isInitializing}, isBuildInProgress: ${editorViewModel.isBuildInProgress}") + val visible = editorViewModel.isBuildInProgress || editorViewModel.isInitializing 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) } + editorViewModel._isBuildInProgress.observe(this) { onBuildStatusChanged() } + editorViewModel._isInitializing.observe(this) { onBuildStatusChanged() } + editorViewModel._statusText.observe(this) { binding.bottomSheet.setStatus(it.first, it.second) } - viewModel.observeFiles(this) { files -> + editorViewModel.observeFiles(this) { files -> binding.apply { if (files.isNullOrEmpty()) { tabs.visibility = View.GONE diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index 5722e1f0ff..2d6d130858 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -95,33 +95,33 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { override fun preDestroy() { super.preDestroy() TSLanguageRegistry.instance.destroy() - viewModel.removeAllFiles() + editorViewModel.removeAllFiles() } override fun onCreate(savedInstanceState: Bundle?) { mBuildEventListener.setActivity(this) super.onCreate(savedInstanceState) - viewModel._displayedFile.observe(this) { this.binding.editorContainer.displayedChild = it } - viewModel._startDrawerOpened.observe(this) { opened -> + editorViewModel._displayedFile.observe(this) { this.binding.editorContainer.displayedChild = it } + editorViewModel._startDrawerOpened.observe(this) { opened -> this.binding.editorDrawerLayout.apply { if (opened) openDrawer(GravityCompat.START) else closeDrawer(GravityCompat.START) } } - viewModel.observeFiles(this) { + editorViewModel.observeFiles(this) { // rewrite the cached files index if there are any opened files val currentFile = getCurrentEditor()?.editor?.file?.absolutePath ?: run { - viewModel.writeOpenedFiles(null) - viewModel.openedFilesCache = null + editorViewModel.writeOpenedFiles(null) + editorViewModel.openedFilesCache = null return@observeFiles } getOpenedFiles().also { val cache = OpenedFilesCache(currentFile, it) - viewModel.writeOpenedFiles(cache) - viewModel.openedFilesCache = cache + editorViewModel.writeOpenedFiles(cache) + editorViewModel.openedFilesCache = cache } } @@ -165,8 +165,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { private fun writeOpenedFilesCache(openedFiles: List, selectedFile: File?) { if (selectedFile == null || openedFiles.isEmpty()) { - viewModel.writeOpenedFiles(null) - viewModel.openedFilesCache = null + editorViewModel.writeOpenedFiles(null) + editorViewModel.openedFilesCache = null log.debug("[onPause]", "No opened files.", "Opened files cache reset to null.") isOpenedFilesSaved.set(true) return @@ -174,9 +174,9 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { val cache = OpenedFilesCache(selectedFile = selectedFile.absolutePath, allFiles = openedFiles) - viewModel.writeOpenedFiles(cache) - viewModel.openedFilesCache = if (!isDestroying) cache else null - log.debug("[onPause]", "Opened files cache reset to ${viewModel.openedFilesCache}") + editorViewModel.writeOpenedFiles(cache) + editorViewModel.openedFilesCache = if (!isDestroying) cache else null + log.debug("[onPause]", "Opened files cache reset to ${editorViewModel.openedFilesCache}") isOpenedFilesSaved.set(true) } @@ -184,17 +184,17 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { super.onStart() try { - if (viewModel.openedFilesCache != null) { - viewModel.openedFilesCache?.let { cache -> + if (editorViewModel.openedFilesCache != null) { + editorViewModel.openedFilesCache?.let { cache -> onReadOpenedFilesCache(cache) } } else { - viewModel.readOpenedFiles { cache -> + editorViewModel.readOpenedFiles { cache -> cache ?: return@readOpenedFiles onReadOpenedFilesCache(cache) } } - viewModel.openedFilesCache = null + editorViewModel.openedFilesCache = null } catch (err: Throwable) { log.error("Failed to reopen recently opened files", err) } @@ -213,8 +213,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } override fun getCurrentEditor(): CodeEditorView? { - return if (viewModel.getCurrentFileIndex() != -1) { - getEditorAtIndex(viewModel.getCurrentFileIndex()) + return if (editorViewModel.getCurrentFileIndex() != -1) { + getEditorAtIndex(editorViewModel.getCurrentFileIndex()) } else null } @@ -252,8 +252,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { tab.select() } - viewModel.startDrawerOpened = false - viewModel.displayedFileIndex = index + editorViewModel.startDrawerOpened = false + editorViewModel.displayedFileIndex = index return try { getEditorAtIndex(index) @@ -274,7 +274,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { return -1 } - val position = viewModel.getOpenedFileCount() + val position = editorViewModel.getOpenedFileCount() log.info("Opening file at index:", position, "file: ", file) @@ -284,8 +284,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { binding.editorContainer.addView(editor) binding.tabs.addTab(binding.tabs.newTab()) - viewModel.addFile(file) - viewModel.setCurrentFile(position, file) + editorViewModel.addFile(file) + editorViewModel.setCurrentFile(position, file) updateTabs() @@ -293,7 +293,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } override fun getEditorForFile(file: File): CodeEditorView? { - for (i in 0 until viewModel.getOpenedFileCount()) { + for (i in 0 until editorViewModel.getOpenedFileCount()) { val editor = binding.editorContainer.getChildAt(i) as CodeEditorView if (file == editor.file) { return editor @@ -308,8 +308,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { return -1 } - for (i in 0 until viewModel.getOpenedFileCount()) { - val opened: File = viewModel.getOpenedFile(i) + for (i in 0 until editorViewModel.getOpenedFileCount()) { + val opened: File = editorViewModel.getOpenedFile(i) if (opened == file) { return i } @@ -326,7 +326,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } if (result.gradleSaved) { - notifySyncNeeded() + editorViewModel.isSyncNeeded = true } if (processResources) { @@ -338,14 +338,14 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { override fun saveAllResult(): SaveResult { val result = SaveResult() - for (i in 0 until viewModel.getOpenedFileCount()) { + for (i in 0 until editorViewModel.getOpenedFileCount()) { saveResult(i, result) } return result } override fun saveResult(index: Int, result: SaveResult) { - if (index < 0 || index >= viewModel.getOpenedFileCount()) { + if (index < 0 || index >= editorViewModel.getOpenedFileCount()) { return } @@ -373,14 +373,14 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } var modified = false - for (file in viewModel.getOpenedFiles()) { + for (file in editorViewModel.getOpenedFiles()) { val editor = getEditorForFile(file) ?: continue modified = modified || editor.isModified } val finalModified = modified ThreadUtils.runOnUiThread { - viewModel.setFilesModified(finalModified) + editorViewModel.setFilesModified(finalModified) // set tab as unmodified val tab = binding.tabs.getTabAt(index) ?: return@runOnUiThread @@ -391,17 +391,17 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } private fun onEditorContentChanged() { - viewModel.setFilesModified(true) + editorViewModel.setFilesModified(true) invalidateOptionsMenu() } override fun areFilesModified(): Boolean { - return viewModel.areFilesModified() + return editorViewModel.areFilesModified() } override fun closeFile(index: Int) { - if (index >= 0 && index < viewModel.getOpenedFileCount()) { - val opened: File = viewModel.getOpenedFile(index) + if (index >= 0 && index < editorViewModel.getOpenedFileCount()) { + val opened: File = editorViewModel.getOpenedFile(index) log.info("Closing file:", opened) val editor = getEditorAtIndex(index) if (editor != null && editor.isModified) { @@ -418,7 +418,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { log.error("Cannot save file before close. Editor instance is null") } - viewModel.removeFile(index) + editorViewModel.removeFile(index) binding.apply { tabs.removeTabAt(index) editorContainer.removeViewAt(index) @@ -437,15 +437,15 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { override fun closeOthers() { val unsavedFiles = - viewModel.getOpenedFiles().map(::getEditorForFile).filter { it != null && it.isModified } + editorViewModel.getOpenedFiles().map(::getEditorForFile).filter { it != null && it.isModified } if (unsavedFiles.isEmpty()) { - val file = viewModel.getCurrentFile() + val file = editorViewModel.getCurrentFile() var index = 0 // keep closing the file at index 0 // if openedFiles[0] == file, then keep closing files at index 1 - while (viewModel.getOpenedFileCount() != 1) { + while (editorViewModel.getOpenedFileCount() != 1) { val editor = getEditorAtIndex(index) // Index of files changes as we keep close files @@ -484,9 +484,9 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } private fun closeAll(runAfter: () -> Unit = {}) { - val count = viewModel.getOpenedFileCount() + val count = editorViewModel.getOpenedFileCount() val unsavedFiles = - viewModel.getOpenedFiles().map(this::getEditorForFile).filter { it != null && it.isModified } + editorViewModel.getOpenedFiles().map(this::getEditorForFile).filter { it != null && it.isModified } if (unsavedFiles.isNotEmpty()) { // There are unsaved files @@ -505,7 +505,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } } - viewModel.removeAllFiles() + editorViewModel.removeAllFiles() binding.apply { tabs.removeAllTabs() tabs.requestLayout() @@ -516,7 +516,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } override fun getOpenedFiles() = - viewModel.getOpenedFiles().mapNotNull { + editorViewModel.getOpenedFiles().mapNotNull { val editor = getEditorForFile(it)?.editor ?: return@mapNotNull null OpenedFile(it.absolutePath, editor.cursorLSPRange) } @@ -562,7 +562,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { } val editor = getEditorAtIndex(index) ?: return - viewModel.updateFile(index, event.newFile) + editorViewModel.updateFile(index, event.newFile) editor.updateFile(event.newFile) updateTabs() @@ -589,7 +589,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { private fun updateTabs() { executeAsyncProvideError({ - val files = viewModel.getOpenedFiles() + val files = editorViewModel.getOpenedFiles() val dupliCount = mutableMapOf() val names = mutableMapOf() val nameBuilder = UniqueNameBuilder("", File.separator) diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt index 8716abceee..14c8e252ea 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt @@ -19,22 +19,27 @@ package com.itsaky.androidide.activities.editor import android.content.Intent import android.os.Bundle +import android.view.Gravity import android.view.ViewGroup.MarginLayoutParams import android.widget.CheckBox +import androidx.activity.viewModels import androidx.annotation.GravityInt import androidx.appcompat.app.AlertDialog import com.blankj.utilcode.util.SizeUtils import com.blankj.utilcode.util.ThreadUtils +import com.google.common.collect.ImmutableList +import com.itsaky.androidide.R import com.itsaky.androidide.R.string import com.itsaky.androidide.databinding.LayoutSearchProjectBinding +import com.itsaky.androidide.flashbar.Flashbar 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.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.gradleInstallationDir import com.itsaky.androidide.preferences.internal.lastOpenedProject import com.itsaky.androidide.projects.ProjectManager import com.itsaky.androidide.projects.ProjectManager.cachedInitResult @@ -44,14 +49,28 @@ 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 +import com.itsaky.androidide.projects.api.AndroidModule import com.itsaky.androidide.projects.api.GradleProject import com.itsaky.androidide.projects.builder.BuildService import com.itsaky.androidide.services.builder.GradleBuildService import com.itsaky.androidide.services.builder.GradleBuildServiceConnnection +import com.itsaky.androidide.tasks.executeAsyncProvideError +import com.itsaky.androidide.tasks.executeWithProgress +import com.itsaky.androidide.tooling.api.IAndroidProject +import com.itsaky.androidide.tooling.api.messages.AndroidInitializationParams +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.api.messages.result.InitializeResult +import com.itsaky.androidide.tooling.api.models.BuildVariantInfo +import com.itsaky.androidide.tooling.api.models.mapToSelectedVariants +import com.itsaky.androidide.utils.DURATION_INDEFINITE import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder import com.itsaky.androidide.utils.RecursiveFileSearcher import com.itsaky.androidide.utils.flashError +import com.itsaky.androidide.utils.flashbarBuilder +import com.itsaky.androidide.utils.resolveAttr +import com.itsaky.androidide.utils.showOnUiThread +import com.itsaky.androidide.utils.withIcon +import com.itsaky.androidide.viewmodel.BuildVariantsViewModel import java.io.File import java.util.concurrent.CompletableFuture import java.util.regex.Pattern @@ -59,10 +78,13 @@ import java.util.stream.Collectors /** @author Akash Yadav */ @Suppress("MemberVisibilityCanBePrivate") -abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { +abstract class ProjectHandlerActivity : BaseEditorActivity() { + + protected val buildVariantsViewModel by viewModels() protected var mSearchingProgress: ProgressSheet? = null protected var mFindInProjectDialog: AlertDialog? = null + protected var syncNotificationFlashbar: Flashbar? = null protected var isFromSavedInstance = false protected var shouldInitialize = false @@ -114,13 +136,28 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { this.isFromSavedInstance = false } + editorViewModel._isSyncNeeded.observe(this) { isSyncNeeded -> + if (!isSyncNeeded) { + // dismiss if already showing + syncNotificationFlashbar?.dismiss() + return@observe + } + + if (syncNotificationFlashbar?.isShowing() == true) { + // already shown + return@observe + } + + notifySyncNeeded() + } + startServices() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.apply { - putBoolean(STATE_KEY_SHOULD_INITIALIZE, !viewModel.isInitializing) + putBoolean(STATE_KEY_SHOULD_INITIALIZE, !editorViewModel.isInitializing) putBoolean(STATE_KEY_FROM_SAVED_INSTANACE, true) } } @@ -133,12 +170,16 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { // of the project ProjectManager.destroy() - viewModel.isInitializing = false - viewModel.isBuildInProgress = false + editorViewModel.isInitializing = false + editorViewModel.isBuildInProgress = false } } override fun preDestroy() { + + syncNotificationFlashbar?.dismiss() + syncNotificationFlashbar = null + if (isDestroying) { releaseServerListener() this.initializingFuture?.cancel(true) @@ -170,26 +211,51 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { (Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) as? GradleBuildService?) ?.setEventListener(null) Lookup.getDefault().unregister(BuildService.KEY_BUILD_SERVICE) - viewModel.isBoundToBuildSerice = false + editorViewModel.isBoundToBuildSerice = false } } } - override fun setStatus(status: CharSequence, @GravityInt gravity: Int) { + fun setStatus(status: CharSequence) { + setStatus(status, Gravity.CENTER) + } + + fun setStatus(status: CharSequence, @GravityInt gravity: Int) { doSetStatus(status, gravity) } - override fun appendBuildOutput(str: String) { + fun appendBuildOutput(str: String) { binding.bottomSheet.appendBuildOut(str) } - override fun notifySyncNeeded() { + fun notifySyncNeeded() { notifySyncNeeded { initializeProject() } } - override fun startServices() { + private fun notifySyncNeeded(onConfirm: () -> Unit) { + val buildService = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) + if (buildService == null || buildService.isBuildInProgress) return + + flashbarBuilder( + duration = DURATION_INDEFINITE, + backgroundColor = resolveAttr(R.attr.colorSecondaryContainer), + messageColor = resolveAttr(R.attr.colorOnSecondaryContainer) + ) + .withIcon(R.drawable.ic_sync, colorFilter = resolveAttr(R.attr.colorOnSecondaryContainer)) + .message(string.msg_sync_needed) + .positiveActionText(string.btn_sync) + .positiveActionTapListener { + onConfirm() + it.dismiss() + } + .negativeActionText(string.btn_ignore_changes) + .negativeActionTapListener(Flashbar::dismiss) + .showOnUiThread() + } + + fun startServices() { val service = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) as GradleBuildService? - if (viewModel.isBoundToBuildSerice && service != null) { + if (editorViewModel.isBoundToBuildSerice && service != null) { log.info("Reusing already started Gradle build service") onGradleBuildServiceConnected(service) return @@ -214,7 +280,74 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { initLspClient() } - override fun initializeProject() { + /** + * Initialize (sync) the project. + * + * @param buildVariantsProvider A function which returns the map of project paths to the selected build variants. + * This function is called asynchronously. + */ + fun initializeProject(buildVariantsProvider: () -> Map) { + executeWithProgress { progress -> + executeAsyncProvideError(buildVariantsProvider::invoke) { result, error -> + com.itsaky.androidide.tasks.runOnUiThread { + progress.dismiss() + } + + if (result == null || error != null) { + val msg = getString(string.msg_build_variants_fetch_failed) + flashError(msg) + log.error(msg, error) + return@executeAsyncProvideError + } + + com.itsaky.androidide.tasks.runOnUiThread { + initializeProject(result) + } + } + } + } + + fun initializeProject() { + val currentVariants = buildVariantsViewModel._buildVariants.value + + // no information about the build variants is available + // use the default variant selections + if (currentVariants == null) { + log.debug( + "No variant selection information available. Default build variants will be selected.") + initializeProject(emptyMap()) + return + } + + // variant selection information is available + // but there are updated & unsaved variant selections + // use the updated variant selections to initialize the project + if (buildVariantsViewModel.updatedBuildVariants.isNotEmpty()) { + val newSelections = currentVariants.toMutableMap() + newSelections.putAll(buildVariantsViewModel.updatedBuildVariants) + initializeProject { + newSelections.mapToSelectedVariants().also { + log.debug("Initializing project with new build variant selections: $it") + } + } + return + } + + // variant selection information is available but no variant selections have been updated + // the user might be trying to sync the project from options menu + // initialize the project with the existing selected variants + initializeProject { + log.debug("Re-initializing project with existing build variant selections") + currentVariants.mapToSelectedVariants() + } + } + + /** + * Initialize (sync) the project. + * + * @param buildVariants A map of project paths to the selected build variants. + */ + fun initializeProject(buildVariants: Map) { val projectDir = File(projectPath) if (!projectDir.exists()) { log.error("GradleProject directory does not exist. Cannot initialize project") @@ -247,7 +380,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { this.initializingFuture = if (shouldInitialize || (!isFromSavedInstance && !initialized)) { log.debug("Sending init request to tooling server..") - buildService.initializeProject(projectDir.absolutePath) + buildService.initializeProject(createProjectInitParams(projectDir, buildVariants)) } else { // The project initialization was in progress before the configuration change // In this case, we should not start another project initialization @@ -273,13 +406,30 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { } } + private fun createProjectInitParams(projectDir: File, + buildVariants: Map): InitializeProjectParams { + return InitializeProjectParams( + projectDir.absolutePath, + gradleInstallationDir, + createAndroidParams(buildVariants) + ) + } + + private fun createAndroidParams(buildVariants: Map): AndroidInitializationParams { + if (buildVariants.isEmpty()) { + return AndroidInitializationParams.DEFAULT + } + + return AndroidInitializationParams(buildVariants) + } + private fun releaseServerListener() { // Release reference to server listener in order to prevent memory leak (Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) as? GradleBuildService?) ?.setServerListener(null) } - override fun stopLanguageServers() { + fun stopLanguageServers() { try { destroyLanguageServers(isChangingConfigurations) } catch (err: Throwable) { @@ -291,7 +441,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { log.info("Connected to Gradle build service") buildServiceConnection.onConnected = null - viewModel.isBoundToBuildSerice = true + editorViewModel.isBoundToBuildSerice = true Lookup.getDefault().update(BuildService.KEY_BUILD_SERVICE, service) service.setEventListener(mBuildEventListener) @@ -311,26 +461,28 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { cachedInitResult = result setupProject() notifyProjectUpdate() + updateBuildVariants() + ThreadUtils.runOnUiThread { postProjectInit(true) } } protected open fun preProjectInit() { setStatus(getString(string.msg_initializing_project)) - viewModel.isInitializing = true + editorViewModel.isInitializing = true } protected open fun postProjectInit(isSuccessful: Boolean) { if (!isSuccessful) { setStatus(getString(string.msg_project_initialization_failed)) flashError(string.msg_project_initialization_failed) - viewModel.isInitializing = false + editorViewModel.isInitializing = false projectInitialized = false return } initialSetup() setStatus(getString(string.msg_project_initialized)) - viewModel.isInitializing = false + editorViewModel.isInitializing = false projectInitialized = true if (mFindInProjectDialog?.isShowing == true) { @@ -340,6 +492,41 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { mFindInProjectDialog = null // Create the dialog again if needed } + private fun updateBuildVariants() { + executeAsyncProvideError({ + rootProject?.run { + val buildVariants = mutableMapOf() + subProjects.forEach { subproject -> + if (subproject is AndroidModule) { + + // variant names are not expected to be modified + val variantNames = ImmutableList.builder() + .addAll(subproject.variants.map { variant -> variant.name }).build() + + val variantName = subproject.selectedVariant?.name + ?: IAndroidProject.DEFAULT_VARIANT + + buildVariants[subproject.path] = + BuildVariantInfo(subproject.path, variantNames, variantName) + } + } + + buildVariants + } + }) { buildVariants, err -> + if (buildVariants == null || err != null) { + log.error("Failed to update build variants list", err) + return@executeAsyncProvideError + } + + // avoid using the 'runOnUiThread' method defined in the activity + com.itsaky.androidide.tasks.runOnUiThread { + buildVariantsViewModel.buildVariants = buildVariants + buildVariantsViewModel.resetUpdatedSelections() + } + } + } + protected open fun createFindInProjectDialog(): AlertDialog? { if (rootProject == null) { log.warn("No root project model found. Is the project initialized?") diff --git a/app/src/main/java/com/itsaky/androidide/adapters/BuildVariantsAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/BuildVariantsAdapter.kt index 11ab53fe2e..e372376073 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/BuildVariantsAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/BuildVariantsAdapter.kt @@ -20,10 +20,16 @@ package com.itsaky.androidide.adapters import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ArrayAdapter +import androidx.core.widget.addTextChangedListener +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.itsaky.androidide.R import com.itsaky.androidide.databinding.LayoutBuildVariantItemBinding +import com.itsaky.androidide.tooling.api.IAndroidProject import com.itsaky.androidide.tooling.api.models.BuildVariantInfo +import com.itsaky.androidide.tooling.api.models.BuildVariantInfo.Companion.withSelection +import com.itsaky.androidide.viewmodel.BuildVariantsViewModel +import java.util.Objects /** * [RecyclerView] adapter for showing the list of Android modules and their selected build variant. @@ -32,10 +38,10 @@ import com.itsaky.androidide.tooling.api.models.BuildVariantInfo * @author Akash Yadav */ class BuildVariantsAdapter( - private val items: List + private val viewModel: BuildVariantsViewModel, + private var items: List ) : RecyclerView.Adapter() { - class ViewHolder(internal val binding: LayoutBuildVariantItemBinding) : RecyclerView.ViewHolder(binding.root) @@ -53,13 +59,43 @@ class BuildVariantsAdapter( val binding = holder.binding val variantInfo = items[position] - binding.moduleName.text = variantInfo.modulePath - binding.variantName.setAdapter( - ArrayAdapter(binding.root.context, R.layout.support_simple_spinner_dropdown_item, - variantInfo.buildVariants + binding.moduleName.text = variantInfo.projectPath + + binding.variantName.apply { + + val viewModel = viewModel + + setAdapter( + ArrayAdapter(binding.root.context, R.layout.support_simple_spinner_dropdown_item, + variantInfo.buildVariants + ) ) - ) - - binding.variantName.listSelection = variantInfo.selectedVariantIndex + + var listSelection = variantInfo.buildVariants.indexOf(variantInfo.selectedVariant) + if (listSelection < 0 || listSelection >= variantInfo.buildVariants.size) { + listSelection = 0 + } + + this.listSelection = listSelection + setText(variantInfo.selectedVariant, false) + + addTextChangedListener { editable -> + // update the changed build variants map + viewModel.updatedBuildVariants = viewModel.updatedBuildVariants.also { variants -> + + // the newly selected build variant + // if this is different that the variant that was used while initializing the project, + // then the user is notified to re-sync the project + // else the selection is cleared + val newSelection = editable?.toString() ?: IAndroidProject.DEFAULT_VARIANT + + if (!Objects.equals(variantInfo.selectedVariant, newSelection)) { + variants[variantInfo.projectPath] = variantInfo.withSelection(newSelection) + } else { + variants.remove(variantInfo.projectPath) + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/fragments/sidebar/BuildVariantsFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/sidebar/BuildVariantsFragment.kt index e6346a996d..162a1e5739 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/sidebar/BuildVariantsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/sidebar/BuildVariantsFragment.kt @@ -20,9 +20,14 @@ package com.itsaky.androidide.fragments.sidebar import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.DividerItemDecoration +import com.itsaky.androidide.activities.editor.ProjectHandlerActivity import com.itsaky.androidide.adapters.BuildVariantsAdapter -import com.itsaky.androidide.fragments.RecyclerViewFragment +import com.itsaky.androidide.databinding.FragmentBuildVariantsBinding +import com.itsaky.androidide.fragments.EmptyStateFragment +import com.itsaky.androidide.tooling.api.models.BuildVariantInfo +import com.itsaky.androidide.tooling.api.models.mapToSelectedVariants +import com.itsaky.androidide.viewmodel.BuildVariantsViewModel import com.itsaky.androidide.viewmodel.EditorViewModel /** @@ -30,20 +35,76 @@ import com.itsaky.androidide.viewmodel.EditorViewModel * * @author Akash Yadav */ -class BuildVariantsFragment : RecyclerViewFragment() { +class BuildVariantsFragment : + EmptyStateFragment(FragmentBuildVariantsBinding::inflate) { - private val viewModel by viewModels(ownerProducer = { requireActivity() }) + private val variantsViewModel by viewModels( + ownerProducer = { requireActivity() } + ) - override fun onCreateAdapter(): RecyclerView.Adapter<*> { - return BuildVariantsAdapter(viewModel.buildVariants) - } + private val editorViewModel by viewModels( + ownerProducer = { requireActivity() } + ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel._buildVariants.observe(viewLifecycleOwner) { + variantsViewModel._buildVariants.observe(viewLifecycleOwner) { + populateRecyclerView() + updateButtonStates(variantsViewModel.updatedBuildVariants) + } + + // observe values and update the button states accordingly + variantsViewModel._updatedBuildVariants.observe(viewLifecycleOwner) { updatedVariants -> + updateButtonStates(updatedVariants) + } + + editorViewModel._isBuildInProgress.observe(viewLifecycleOwner) { + updateButtonStates(variantsViewModel.updatedBuildVariants) + } - // Setup the recyclerview each time build variants list is updated - onSetupRecyclerView() + editorViewModel._isInitializing.observe(viewLifecycleOwner) { + updateButtonStates(variantsViewModel.updatedBuildVariants) } + + binding.apply.setOnClickListener { + (activity as? ProjectHandlerActivity?)?.initializeProject() + } + + binding.discard.setOnClickListener { + variantsViewModel.resetUpdatedSelections() + populateRecyclerView() + } + + binding.variantsList.addItemDecoration( + DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL) + ) + + populateRecyclerView() + } + + private fun updateButtonStates( + updatedVariants: MutableMap? + ) { + _binding?.apply { + // enable buttons only if any of the project's selected build variant was changed + // also, changes can only if be applied if no build is in progress + val isBuilding = editorViewModel.let { it.isBuildInProgress || it.isInitializing } + val isEnabled = updatedVariants?.isNotEmpty() == true && !isBuilding + + apply.isEnabled = isEnabled + discard.isEnabled = isEnabled + } + } + + private fun populateRecyclerView() { + _binding?.variantsList?.apply { + this.adapter = BuildVariantsAdapter(variantsViewModel, + variantsViewModel.buildVariants.values.toList()) + checkIsEmpty() + } + } + + private fun checkIsEmpty() { + emptyStateViewModel.isEmpty.value = _binding?.variantsList?.adapter?.itemCount == 0 } } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/handlers/EditorBuildEventListener.kt b/app/src/main/java/com/itsaky/androidide/handlers/EditorBuildEventListener.kt index 581be17cf1..35d81769a1 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/EditorBuildEventListener.kt +++ b/app/src/main/java/com/itsaky/androidide/handlers/EditorBuildEventListener.kt @@ -51,7 +51,7 @@ class EditorBuildEventListener : GradleBuildService.EventListener { activity().showFirstBuildNotice() } - activity().viewModel.isBuildInProgress = true + activity().editorViewModel.isBuildInProgress = true activity().binding.bottomSheet.clearBuildOutput() if (buildInfo.tasks.isNotEmpty()) { @@ -64,7 +64,7 @@ class EditorBuildEventListener : GradleBuildService.EventListener { analyzeCurrentFile() isFirstBuild = false - activity().viewModel.isBuildInProgress = false + activity().editorViewModel.isBuildInProgress = false } override fun onProgressEvent(event: ProgressEvent) { @@ -77,7 +77,7 @@ class EditorBuildEventListener : GradleBuildService.EventListener { analyzeCurrentFile() isFirstBuild = false - activity().viewModel.isBuildInProgress = false + activity().editorViewModel.isBuildInProgress = false } override fun onOutput(line: String?) { diff --git a/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt b/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt deleted file mode 100644 index 4fe4c63c5e..0000000000 --- a/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.interfaces - -import android.view.Gravity -import androidx.core.view.GravityCompat - -/** - * - * @author Akash Yadav - */ -interface IProjectHandler { - - fun setStatus(status: CharSequence) = setStatus(status, Gravity.CENTER) - fun setStatus(status: CharSequence, gravity: Int) - fun appendBuildOutput(str: String) - - fun notifySyncNeeded() - - fun startServices() - fun initializeProject() - fun stopLanguageServers() -} \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java index 36861b124a..c2fe35c7a3 100755 --- a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java +++ b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java @@ -380,7 +380,7 @@ private Map> filterRelevantDiagnostics( @NonNull private Set findOpenFiles(final Set files, final int max) { - final var openedFiles = activity().getViewModel().getOpenedFiles(); + final var openedFiles = activity().getEditorViewModel().getOpenedFiles(); final var result = new TreeSet(); for (int i = 0; i < openedFiles.size(); i++) { final var opened = openedFiles.get(i); diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java index a3f98e4676..2dd6ce701e 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java @@ -401,10 +401,10 @@ public boolean isBuildInProgress() { @NonNull @Override - public CompletableFuture initializeProject(@NonNull String rootDir) { + public CompletableFuture initializeProject(@NonNull InitializeProjectParams params) { checkServerStarted(); - final var message = new InitializeProjectParams(rootDir, "", getGradleInstallationDir()); - return performBuildTasks(server.initialize(message)); + Objects.requireNonNull(params); + return performBuildTasks(server.initialize(params)); } @NonNull diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/BuildVariantsViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/BuildVariantsViewModel.kt new file mode 100644 index 0000000000..053509c72a --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/BuildVariantsViewModel.kt @@ -0,0 +1,52 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.itsaky.androidide.tooling.api.models.BuildVariantInfo + +/** + * [ViewModel] for the build variants fragment. + * + * @author Akash Yadav + */ +class BuildVariantsViewModel : ViewModel() { + + internal val _buildVariants = MutableLiveData>(null) + internal val _updatedBuildVariants = MutableLiveData>(null) + + var buildVariants: Map + get() = this._buildVariants.value ?: emptyMap() + set(value) { + this._buildVariants.value = value + } + + var updatedBuildVariants: MutableMap + get() = this._updatedBuildVariants.value ?: mutableMapOf() + set(value) { + this._updatedBuildVariants.value = value + } + + /** + * Resets the updated selections. + */ + internal fun resetUpdatedSelections() { + updatedBuildVariants = updatedBuildVariants.also { it.clear() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt index ab7705661b..9b8e4bd0f6 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt @@ -40,6 +40,7 @@ class EditorViewModel : ViewModel() { internal val _statusText = MutableLiveData>("" to CENTER) internal val _displayedFile = MutableLiveData(-1) internal val _startDrawerOpened = MutableLiveData(false) + internal val _isSyncNeeded = MutableLiveData(false) private val _openedFiles = MutableLiveData() private val _isBoundToBuildService = MutableLiveData(false) @@ -53,8 +54,6 @@ class EditorViewModel : ViewModel() { */ private val mCurrentFile = MutableLiveData?>(null) - internal val _buildVariants = MutableLiveData>(null) - var openedFilesCache: OpenedFilesCache? get() = _openedFiles.value set(value) { @@ -103,10 +102,10 @@ class EditorViewModel : ViewModel() { _startDrawerOpened.value = value } - var buildVariants: List - get() = this._buildVariants.value ?: emptyList() + var isSyncNeeded: Boolean + get() = _isSyncNeeded.value ?: false set(value) { - this._buildVariants.value = value + _isSyncNeeded.value = value } /** diff --git a/app/src/main/res/layout/fragment_build_variants.xml b/app/src/main/res/layout/fragment_build_variants.xml new file mode 100644 index 0000000000..3e66090a3e --- /dev/null +++ b/app/src/main/res/layout/fragment_build_variants.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_build_variant_item.xml b/app/src/main/res/layout/layout_build_variant_item.xml index c192c5fc6e..d43c29fdeb 100644 --- a/app/src/main/res/layout/layout_build_variant_item.xml +++ b/app/src/main/res/layout/layout_build_variant_item.xml @@ -19,7 +19,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:paddingStart="16dp" + android:paddingEnd="16dp"> Enable LogSender When disabled, logs from applications won\'t be shown in AndroidIDE Build variants + Apply + Discard + Failed to fetch build variants diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt index 43316febd4..7e8d4814a9 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt @@ -30,8 +30,10 @@ 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.tasks.executeAsync +import com.itsaky.androidide.tooling.api.IAndroidProject import com.itsaky.androidide.tooling.api.IProject import com.itsaky.androidide.tooling.api.messages.result.InitializeResult +import com.itsaky.androidide.tooling.api.models.BuildVariantInfo import com.itsaky.androidide.utils.DocumentUtils import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.flashError @@ -67,7 +69,8 @@ object ProjectManager : EventReceiver { project: IProject = Lookup.getDefault().lookup(BuildService.KEY_PROJECT_PROXY)!!, isInitialized: Boolean = true ) { - this.rootProject = if (isInitialized) ProjectTransformer().transform(CachingProject(project)) else null + this.rootProject = if (isInitialized) ProjectTransformer().transform( + CachingProject(project)) else null if (this.rootProject != null) { this.app = this.rootProject!!.findFirstAndroidAppModule() this.rootProject!!.subProjects.filterIsInstance(ModuleProject::class.java).forEach { @@ -156,11 +159,10 @@ object ProjectManager : EventReceiver { fun notifyProjectUpdate() { executeAsync { - if (rootProject != null) { - // Update the source file index - rootProject!!.subProjects.forEach { - if (it is ModuleProject) { - it.indexSources() + rootProject?.apply { + subProjects.forEach { subproject -> + if (subproject is ModuleProject) { + subproject.indexSources() } } } diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt index 1d856544f5..37fd509f25 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt @@ -103,6 +103,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this.. val lintCheckJars: List, val modelSyncFiles: List, val variants: List = listOf(), + val selectedVariant: BasicAndroidVariantMetadata?, val classesJar: File? ) : ModuleProject( diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/builder/BuildService.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/builder/BuildService.kt index 82b5ee7279..caa86fa426 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/builder/BuildService.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/builder/BuildService.kt @@ -20,6 +20,7 @@ package com.itsaky.androidide.projects.builder import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lookup.Lookup.Key import com.itsaky.androidide.tooling.api.IProject +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.api.messages.result.BuildCancellationRequestResult import com.itsaky.androidide.tooling.api.messages.result.InitializeResult import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult @@ -54,11 +55,11 @@ interface BuildService { /** * Initialize the project. * - * @param rootDir The root directory of the project to initialize. + * @param params Parameters for the project initialization. * @return A [CompletableFuture] which returns an [InitializeResult] when the project * initialization process finishes. */ - fun initializeProject(rootDir: String): CompletableFuture + fun initializeProject(params: InitializeProjectParams): CompletableFuture /** * Execute the given tasks. diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/util/ProjectTransformer.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/util/ProjectTransformer.kt index 0425212375..e70d5e165e 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/util/ProjectTransformer.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/util/ProjectTransformer.kt @@ -86,6 +86,8 @@ class ProjectTransformer { ): AndroidModule { val metadata = project.getMetadata().get() as AndroidProjectMetadata val libraryMap = project.getLibraryMap().get() + val variants = project.getVariants().get() + val selectedVariant = project.getSelectedVariant().get() return AndroidModule( name = metadata.name ?: IProject.PROJECT_UNKNOWN, description = metadata.description ?: "", @@ -109,7 +111,8 @@ class ProjectTransformer { libraryMap = libraryMap, lintCheckJars = project.getLintCheckJars().get(), modelSyncFiles = project.getModelSyncFiles().get(), - variants = project.getVariants().get(), + variants = variants, + selectedVariant = variants.find { it.name == selectedVariant }, classesJar = metadata.classesJar ) } diff --git a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt index 9a8f5b3b48..82a068b2ab 100644 --- a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt +++ b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt @@ -46,7 +46,7 @@ class ModuleProjectTest { @Test fun test() { val (server, project) = ToolingApiTestLauncher().launchServer() - server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString, "")).get() + server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString)).get() Lookup.getDefault().register(BuildService.KEY_PROJECT_PROXY, project) diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt index 10a2769f21..3d2f7949b7 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt @@ -100,7 +100,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) : stopWatch.lapFromLast("Project connection established") val project = try { - val impl = RootModelBuilder.build(connection to params.androidVariant) as? ProjectImpl? + val impl = RootModelBuilder(params).build(connection) as? ProjectImpl? ?: throw ModelBuilderException("Failed to build project model") impl } catch (err: Throwable) { diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt index 83f8326e95..2c2991f64c 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt @@ -50,7 +50,7 @@ import java.util.concurrent.CompletableFuture */ internal class AndroidProjectImpl( gradleProject: GradleProject, - private val variant: String, + private val selectedVariant: String, private val basicAndroidProject: BasicAndroidProject, private val androidProject: AndroidProject, private val variantDependencies: VariantDependencies @@ -68,7 +68,9 @@ internal class AndroidProjectImpl( return field } - + override fun getSelectedVariant(): CompletableFuture { + return CompletableFuture.completedFuture(this.selectedVariant) + } override fun getVariants(): CompletableFuture> { return CompletableFuture.supplyAsync { @@ -161,14 +163,15 @@ internal class AndroidProjectImpl( private fun getClassesJar(): File { // TODO(itsaky): this should handle product flavors as well return File(gradleProject.buildDirectory, - "${IAndroidProject.FD_INTERMEDIATES}/compile_library_classes_jar/$variant/classes.jar") + "${IAndroidProject.FD_INTERMEDIATES}/compile_library_classes_jar/$selectedVariant/classes.jar") } override fun getClasspaths(): CompletableFuture> { return CompletableFuture.supplyAsync { mutableListOf().apply { add(getClassesJar()) - getVariant(StringParameter(variant)).get()?.mainArtifact?.classJars?.let(this::addAll) + getVariant(StringParameter(selectedVariant)).get()?.mainArtifact?.classJars?.let( + this::addAll) } } } diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/forwarding/ForwardingProject.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/forwarding/ForwardingProject.kt index a4e8cb2c1b..c599cb78bc 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/forwarding/ForwardingProject.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/forwarding/ForwardingProject.kt @@ -57,6 +57,12 @@ internal class ForwardingProject(var project: IGradleProject? = null) : IGradleP UnsupportedOperationException()) } + override fun getSelectedVariant(): CompletableFuture { + return this.androidProject?.getSelectedVariant() ?: CompletableFuture.failedFuture( + UnsupportedOperationException() + ) + } + override fun getVariants(): CompletableFuture> { return this.androidProject?.getVariants() ?: CompletableFuture.failedFuture( UnsupportedOperationException()) diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt index fe401b63f8..63fe8a4d31 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt @@ -17,6 +17,7 @@ package com.itsaky.androidide.tooling.impl.sync import com.android.builder.model.v2.models.Versions +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.impl.Main import com.itsaky.androidide.tooling.impl.util.StopWatch import com.itsaky.androidide.utils.ILogger @@ -33,8 +34,9 @@ import org.gradle.tooling.model.Model * @property androidVariant The name of the variant for which the Android models will be built. * @author Akash Yadav */ -abstract class AbstractModelBuilder(protected val androidVariant: String = "") : - IModelBuilder { +abstract class AbstractModelBuilder( + protected val initializationParams: InitializeProjectParams +) : IModelBuilder { companion object { diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt index b90e4346d7..cc02e47a2e 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt @@ -21,6 +21,7 @@ import com.android.builder.model.v2.models.BasicAndroidProject import com.android.builder.model.v2.models.ModelBuilderParameter import com.android.builder.model.v2.models.VariantDependencies import com.itsaky.androidide.tooling.api.IAndroidProject +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.impl.internal.AndroidProjectImpl /** @@ -28,27 +29,29 @@ import com.itsaky.androidide.tooling.impl.internal.AndroidProjectImpl * * @author Akash Yadav */ -class AndroidProjectModelBuilder(androidVariant: String) : - AbstractModelBuilder(androidVariant) { +class AndroidProjectModelBuilder(initializationParams: InitializeProjectParams) : + AbstractModelBuilder(initializationParams) { override fun build(param: BuildControllderAndIdeaModule): IAndroidProject { val (controller, module) = param - - val projectpath = module.gradleProject.path + + val androidParams = initializationParams.androidParams + val projectPath = module.gradleProject.path val basicModel = controller.getModelAndLog(module, BasicAndroidProject::class.java) val androidModel = controller.getModelAndLog(module, AndroidProject::class.java) val variantNames = basicModel.variants.map { it.name } log( - "${variantNames.size} build variants found for project '$projectpath': $variantNames") + "${variantNames.size} build variants found for project '$projectPath': $variantNames") - val selectedVariant = androidVariant.ifBlank { variantNames.firstOrNull() } + val androidVariant = androidParams.variantSelections[projectPath] + val selectedVariant = androidVariant ?: variantNames.firstOrNull() if (selectedVariant.isNullOrBlank()) { throw ModelBuilderException( - "No variant found for project '$projectpath'. providedVariant=$androidVariant") + "No variant found for project '$projectPath'. providedVariant=$androidVariant") } - log("Selected build variant '$selectedVariant' for project '$projectpath'") + log("Selected build variant '$selectedVariant' for project '$projectPath'") val variantDependencies = controller.getModelAndLog(module, VariantDependencies::class.java, ModelBuilderParameter::class.java) { diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt index 58dcfacd29..68b4f0c876 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt @@ -17,6 +17,7 @@ package com.itsaky.androidide.tooling.impl.sync import com.itsaky.androidide.tooling.api.IGradleProject +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.impl.internal.GradleProjectImpl import org.gradle.tooling.model.GradleProject @@ -25,7 +26,9 @@ import org.gradle.tooling.model.GradleProject * * @author Akash Yadav */ -class GradleProjectModelBuilder : AbstractModelBuilder() { +class GradleProjectModelBuilder(initializationParams: InitializeProjectParams) : + AbstractModelBuilder( + initializationParams) { @Throws(ModelBuilderException::class) override fun build(param: GradleProject): IGradleProject { diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt index 485a7a65a9..5d01a709df 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt @@ -19,6 +19,7 @@ package com.itsaky.androidide.tooling.impl.sync import com.itsaky.androidide.builder.model.IJavaCompilerSettings import com.itsaky.androidide.tooling.api.IJavaProject +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.api.models.JavaModuleCompilerSettings import com.itsaky.androidide.tooling.impl.internal.JavaProjectImpl import org.gradle.tooling.model.idea.IdeaModule @@ -29,8 +30,8 @@ import org.gradle.tooling.model.idea.IdeaProject * * @author Akash Yadav */ -class JavaProjectModelBuilder : - AbstractModelBuilder() { +class JavaProjectModelBuilder(initializationParams: InitializeProjectParams) : + AbstractModelBuilder(initializationParams) { override fun build(param: JavaProjectModelBuilderParams): IJavaProject { val compilerSettings = createCompilerSettings(param.project, param.module) diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt index 5b0f113881..b3f0b26a63 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt @@ -18,22 +18,23 @@ package com.itsaky.androidide.tooling.impl.sync import com.itsaky.androidide.tooling.api.IModuleProject +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams /** * Builds models for module projects (either Android app/library or Java library projects). * * @author Akash Yadav */ -class ModuleProjectModelBuilder(androidVariant: String = "") : - AbstractModelBuilder(androidVariant) { +class ModuleProjectModelBuilder(initializationParams: InitializeProjectParams) : + AbstractModelBuilder(initializationParams) { override fun build(param: ModuleProjectModelBuilderParams): IModuleProject { val isAndroidProject = getAndroidVersions(param.module, param.controller) != null return if (isAndroidProject) { - AndroidProjectModelBuilder(androidVariant).build( + AndroidProjectModelBuilder(initializationParams).build( param.controller to param.module) } else { - JavaProjectModelBuilder().build( + JavaProjectModelBuilder(initializationParams).build( JavaProjectModelBuilderParams(param)) } } diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt index 554664ad2e..1feee44602 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt @@ -19,23 +19,32 @@ package com.itsaky.androidide.tooling.impl.sync import com.itsaky.androidide.tooling.api.IAndroidProject import com.itsaky.androidide.tooling.api.IProject +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.impl.Main import com.itsaky.androidide.tooling.impl.Main.finalizeLauncher import com.itsaky.androidide.tooling.impl.internal.ProjectImpl import com.itsaky.androidide.utils.ILogger import org.gradle.tooling.ConfigurableLauncher +import org.gradle.tooling.ProjectConnection import org.gradle.tooling.model.idea.IdeaProject +import java.io.Serializable /** * Utility class to build the project models. * * @author Akash Yadav */ -object RootModelBuilder : AbstractModelBuilder("") { +class RootModelBuilder(initializationParams: InitializeProjectParams) : + AbstractModelBuilder(initializationParams), Serializable { - override fun build(param: ProjectConnectionAndAndroidVariant): IProject { - val (connection, androidVariant) = param - val executor = connection.action { controller -> + private val serialVersionUID = 1L + + override fun build(param: ProjectConnection): IProject { + + // do not reference the 'initializationParams' field in the + val initializationParams = initializationParams + + val executor = param.action { controller -> val ideaProject = controller.getModelAndLog(IdeaProject::class.java) val ideaModules = ideaProject.modules @@ -49,17 +58,17 @@ object RootModelBuilder : AbstractModelBuilder - ModuleProjectModelBuilder(androidVariant).build( + ModuleProjectModelBuilder(initializationParams).build( ModuleProjectModelBuilderParams(controller, ideaProject, ideaModule, modulePaths)) } - ProjectImpl(rootProject, projects) + return@action ProjectImpl(rootProject, projects) } finalizeLauncher(executor) diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt index fce2b857d7..04da5d9a0b 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt @@ -17,6 +17,7 @@ package com.itsaky.androidide.tooling.impl.sync +import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import org.gradle.tooling.BuildController import org.gradle.tooling.ProjectConnection import org.gradle.tooling.model.idea.IdeaModule @@ -31,7 +32,7 @@ class ModuleProjectModelBuilderParams(val controller: BuildController, project: open class JavaProjectModelBuilderParams(val project: IdeaProject, val module: IdeaModule, val modulePaths: Map) { - constructor(base: ModuleProjectModelBuilderParams) : this(base.project, base.module, base.modulePaths) -} -typealias ProjectConnectionAndAndroidVariant = Pair \ No newline at end of file + constructor(base: ModuleProjectModelBuilderParams) : this(base.project, base.module, + base.modulePaths) +} \ No newline at end of file diff --git a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt index ffb6b59bab..abad32e4df 100644 --- a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt +++ b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt @@ -47,7 +47,7 @@ class MultiModuleAndroidProjectTest { @Test fun `test simple multi module project initialization`() { val (server, project) = ToolingApiTestLauncher().launchServer() - server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString, "")).get() + server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString)).get() doAssertions(project, server) } @@ -197,8 +197,7 @@ class MultiModuleAndroidProjectTest { client.agpVersion = agpVersion client.gradleVersion = gradleVersion val (server, project) = ToolingApiTestLauncher().launchServer(client = client) - server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString, - "")).get() + server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString)).get() doAssertions(project = project, server = server) FileProvider.testProjectRoot().resolve(MultiVersionTestClient.buildFile).deleteExisting() } diff --git a/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/IAndroidProject.kt b/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/IAndroidProject.kt index b8e6ed46d1..b9d8fac0b9 100644 --- a/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/IAndroidProject.kt +++ b/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/IAndroidProject.kt @@ -36,6 +36,12 @@ import java.util.concurrent.CompletableFuture @JsonSegment("android") interface IAndroidProject : IModuleProject { + /** + * Get the variant that was selected while building the model for this project. + */ + @JsonRequest + fun getSelectedVariant(): CompletableFuture + /** * Get the metadata about all variants of this Android project. */ @@ -87,6 +93,11 @@ interface IAndroidProject : IModuleProject { @Suppress("unused") companion object { + /** + * The name of the Android project build variant that is used by default. + */ + const val DEFAULT_VARIANT = "debug" + const val ANDROID_NAMESPACE = "http://schemas.android.com/res/android" // Injectable properties to use with -P diff --git a/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/BuildVariantInfo.kt b/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/BuildVariantInfo.kt index 94e2fb98b7..f9327842f0 100644 --- a/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/BuildVariantInfo.kt +++ b/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/BuildVariantInfo.kt @@ -20,10 +20,37 @@ package com.itsaky.androidide.tooling.api.models /** * Information about the build variants of an Android module. * - * @property modulePath The project path of the Android module project. + * @property projectPath The project path of the Android module project. * @property buildVariants The build variants of the Android module project. - * @property selectedVariantIndex The index of the build variant in the [buildVariants] list which is currently selected. + * @property selectedVariant The name of the selected build variant. * @author Akash Yadav */ -data class BuildVariantInfo(val modulePath: String, val buildVariants: List, - val selectedVariantIndex: Int) +data class BuildVariantInfo( + val projectPath: String, + val buildVariants: List, + val selectedVariant: String +) { + + companion object { + + /** + * Creates a new [BuildVariantInfo] object with the given selected variants. All the properties + * of this [BuildVariantInfo] is copied to the new [BuildVariantInfo] and the [newSelection] is + * set as the [BuildVariantInfo.selectedVariant]. + */ + @JvmStatic + fun BuildVariantInfo.withSelection(newSelection: String): BuildVariantInfo { + require(this.buildVariants.indexOf(newSelection) != -1) { + "'$newSelection' is not a valid variant name. Available variants: ${this.buildVariants}" + } + return BuildVariantInfo(this.projectPath, this.buildVariants, newSelection) + } + } +} + +/** + * Maps the values to the selected variant names. + */ +fun Map.mapToSelectedVariants(): Map { + return mapValues { it.value.selectedVariant } +} diff --git a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/InitializeProjectParams.kt b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/InitializeProjectParams.kt index 599665987f..3cda97ac83 100644 --- a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/InitializeProjectParams.kt +++ b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/InitializeProjectParams.kt @@ -17,13 +17,36 @@ package com.itsaky.androidide.tooling.api.messages +import java.io.Serializable + /** * Message sent from client to server to initialize the tooling API client in the given directory. * + * @property directory The absolute path to the root directory of the project to initialize. + * @property gradleInstallation The installation path of the Gradle distribution to use. + * @property androidParams The [AndroidInitializationParams] for initializing the Android module projects. * @author Akash Yadav */ -data class InitializeProjectParams( +data class InitializeProjectParams @JvmOverloads constructor( val directory: String, - val androidVariant: String, - val gradleInstallation: String = "" -) + val gradleInstallation: String = "", + val androidParams: AndroidInitializationParams = AndroidInitializationParams.DEFAULT +) : Serializable + +/** + * Initialization params for Android project/modules. + * + * @property variantSelections The map of module paths to the name of the variants which should + * be fetched/initialized. + */ +data class AndroidInitializationParams(val variantSelections: Map) : Serializable { + + companion object { + + /** + * Default initialization params. This initializes the Android modules with default values. + */ + @JvmStatic + val DEFAULT = AndroidInitializationParams(emptyMap()) + } +} diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt index 559ad67235..910587aeeb 100644 --- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt +++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt @@ -45,7 +45,7 @@ object XmlInflaterTest { val (server, project) = ToolingApiTestLauncher().launchServer() - server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString, "")).get() + server.initialize(InitializeProjectParams(FileProvider.testProjectRoot().pathString)).get() Lookup.getDefault().register(BuildService.KEY_PROJECT_PROXY, project) ProjectManager.setupProject()