feat: add support for selecting build variants in project (closes #223)

This commit is contained in:
Akash Yadav
2023-08-26 19:49:10 +05:30
parent 73f2b18a37
commit 4b577f5eed
39 changed files with 691 additions and 237 deletions
@@ -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) {
@@ -48,7 +48,7 @@ abstract class FileTabAction : EditorActivityAction() {
return
}
visible = activity.viewModel.getOpenedFiles().isNotEmpty()
visible = activity.editorViewModel.getOpenedFiles().isNotEmpty()
enabled = visible
}
@@ -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()
@@ -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<Intent>? = null
val viewModel by viewModels<EditorViewModel>()
val editorViewModel by viewModels<EditorViewModel>()
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
@@ -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<OpenedFile>, 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<String, Int>()
val names = mutableMapOf<Int, String>()
val nameBuilder = UniqueNameBuilder<File>("", File.separator)
@@ -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<BuildVariantsViewModel>()
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<String, String>) {
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<String, String>) {
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<String, String>): InitializeProjectParams {
return InitializeProjectParams(
projectDir.absolutePath,
gradleInstallationDir,
createAndroidParams(buildVariants)
)
}
private fun createAndroidParams(buildVariants: Map<String, String>): 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<String, BuildVariantInfo>()
subProjects.forEach { subproject ->
if (subproject is AndroidModule) {
// variant names are not expected to be modified
val variantNames = ImmutableList.builder<String>()
.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?")
@@ -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<BuildVariantInfo>
private val viewModel: BuildVariantsViewModel,
private var items: List<BuildVariantInfo>
) : RecyclerView.Adapter<BuildVariantsAdapter.ViewHolder>() {
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)
}
}
}
}
}
}
@@ -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<BuildVariantsAdapter>() {
class BuildVariantsFragment :
EmptyStateFragment<FragmentBuildVariantsBinding>(FragmentBuildVariantsBinding::inflate) {
private val viewModel by viewModels<EditorViewModel>(ownerProducer = { requireActivity() })
private val variantsViewModel by viewModels<BuildVariantsViewModel>(
ownerProducer = { requireActivity() }
)
override fun onCreateAdapter(): RecyclerView.Adapter<*> {
return BuildVariantsAdapter(viewModel.buildVariants)
}
private val editorViewModel by viewModels<EditorViewModel>(
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)
}
// Setup the recyclerview each time build variants list is updated
onSetupRecyclerView()
// observe values and update the button states accordingly
variantsViewModel._updatedBuildVariants.observe(viewLifecycleOwner) { updatedVariants ->
updateButtonStates(updatedVariants)
}
editorViewModel._isBuildInProgress.observe(viewLifecycleOwner) {
updateButtonStates(variantsViewModel.updatedBuildVariants)
}
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<String, BuildVariantInfo>?
) {
_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
}
}
@@ -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?) {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
@@ -380,7 +380,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
@NonNull
private Set<File> findOpenFiles(final Set<File> files, final int max) {
final var openedFiles = activity().getViewModel().getOpenedFiles();
final var openedFiles = activity().getEditorViewModel().getOpenedFiles();
final var result = new TreeSet<File>();
for (int i = 0; i < openedFiles.size(); i++) {
final var opened = openedFiles.get(i);
@@ -401,10 +401,10 @@ public class GradleBuildService extends Service implements BuildService, IToolin
@NonNull
@Override
public CompletableFuture<InitializeResult> initializeProject(@NonNull String rootDir) {
public CompletableFuture<InitializeResult> 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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Map<String, BuildVariantInfo>>(null)
internal val _updatedBuildVariants = MutableLiveData<MutableMap<String, BuildVariantInfo>>(null)
var buildVariants: Map<String, BuildVariantInfo>
get() = this._buildVariants.value ?: emptyMap()
set(value) {
this._buildVariants.value = value
}
var updatedBuildVariants: MutableMap<String, BuildVariantInfo>
get() = this._updatedBuildVariants.value ?: mutableMapOf()
set(value) {
this._updatedBuildVariants.value = value
}
/**
* Resets the updated selections.
*/
internal fun resetUpdatedSelections() {
updatedBuildVariants = updatedBuildVariants.also { it.clear() }
}
}
@@ -40,6 +40,7 @@ class EditorViewModel : ViewModel() {
internal val _statusText = MutableLiveData<Pair<CharSequence, @GravityInt Int>>("" to CENTER)
internal val _displayedFile = MutableLiveData(-1)
internal val _startDrawerOpened = MutableLiveData(false)
internal val _isSyncNeeded = MutableLiveData(false)
private val _openedFiles = MutableLiveData<OpenedFilesCache>()
private val _isBoundToBuildService = MutableLiveData(false)
@@ -53,8 +54,6 @@ class EditorViewModel : ViewModel() {
*/
private val mCurrentFile = MutableLiveData<Pair<Int, File?>?>(null)
internal val _buildVariants = MutableLiveData<List<BuildVariantInfo>>(null)
var openedFilesCache: OpenedFilesCache?
get() = _openedFiles.value
set(value) {
@@ -103,10 +102,10 @@ class EditorViewModel : ViewModel() {
_startDrawerOpened.value = value
}
var buildVariants: List<BuildVariantInfo>
get() = this._buildVariants.value ?: emptyList()
var isSyncNeeded: Boolean
get() = _isSyncNeeded.value ?: false
set(value) {
this._buildVariants.value = value
_isSyncNeeded.value = value
}
/**
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/variants_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingBottom="16dp"
android:clipChildren="false"
android:clipToPadding="false"
android:fadingEdge="vertical"
android:fadingEdgeLength="16dp"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@id/apply"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/layout_build_variant_item" />
<com.google.android.material.button.MaterialButton
android:id="@+id/discard"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:text="@string/title_discard"
app:icon="@drawable/ic_close"
app:iconGravity="textStart"
app:layout_constraintBottom_toBottomOf="@id/apply"
app:layout_constraintEnd_toStartOf="@id/apply"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/apply"
style="@style/Widget.Material3.Button.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:text="@string/title_apply"
app:icon="@drawable/ic_ok"
app:iconGravity="textStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/discard" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -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">
<TextView
android:id="@+id/module_name"
@@ -38,6 +42,8 @@
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:boxStrokeWidth="0dp"
app:boxStrokeColor="@android:color/transparent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/module_name"
app:layout_constraintTop_toTopOf="parent"
@@ -0,0 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "7.2.0" apply false
id("com.android.library") version "7.2.0" apply false
}
tasks.register<Delete>("clean") {
delete(rootProject.buildDir)
}
@@ -92,8 +92,7 @@ abstract class LSPTest {
Lookup.getDefault().update(BuildService.KEY_PROJECT_PROXY, project)
server
.initialize(InitializeProjectParams(FileProvider.testProjectRoot().toFile().absolutePath,
""))
.initialize(InitializeProjectParams(FileProvider.testProjectRoot().toFile().absolutePath))
.get()
Environment.ANDROID_JAR = FileProvider.resources().resolve("android.jar").toFile()
@@ -595,4 +595,7 @@
<string name="idepref_devOptions_enableLogsender_title">Enable LogSender</string>
<string name="idepref_devOptions_enableLogsender_summary">When disabled, logs from applications won\'t be shown in AndroidIDE</string>
<string name="title_build_variants">Build variants</string>
<string name="title_apply">Apply</string>
<string name="title_discard">Discard</string>
<string name="msg_build_variants_fetch_failed">Failed to fetch build variants</string>
</resources>
@@ -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()
}
}
}
@@ -103,6 +103,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
val lintCheckJars: List<File>,
val modelSyncFiles: List<DefaultModelSyncFile>,
val variants: List<BasicAndroidVariantMetadata> = listOf(),
val selectedVariant: BasicAndroidVariantMetadata?,
val classesJar: File?
) :
ModuleProject(
@@ -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<InitializeResult>
fun initializeProject(params: InitializeProjectParams): CompletableFuture<InitializeResult>
/**
* Execute the given tasks.
@@ -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
)
}
@@ -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)
@@ -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) {
@@ -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<String> {
return CompletableFuture.completedFuture(this.selectedVariant)
}
override fun getVariants(): CompletableFuture<List<BasicAndroidVariantMetadata>> {
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<List<File>> {
return CompletableFuture.supplyAsync {
mutableListOf<File>().apply {
add(getClassesJar())
getVariant(StringParameter(variant)).get()?.mainArtifact?.classJars?.let(this::addAll)
getVariant(StringParameter(selectedVariant)).get()?.mainArtifact?.classJars?.let(
this::addAll)
}
}
}
@@ -57,6 +57,12 @@ internal class ForwardingProject(var project: IGradleProject? = null) : IGradleP
UnsupportedOperationException())
}
override fun getSelectedVariant(): CompletableFuture<String> {
return this.androidProject?.getSelectedVariant() ?: CompletableFuture.failedFuture(
UnsupportedOperationException()
)
}
override fun getVariants(): CompletableFuture<List<BasicAndroidVariantMetadata>> {
return this.androidProject?.getVariants() ?: CompletableFuture.failedFuture(
UnsupportedOperationException())
@@ -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<P, R>(protected val androidVariant: String = "") :
IModelBuilder<P, R> {
abstract class AbstractModelBuilder<P, R>(
protected val initializationParams: InitializeProjectParams
) : IModelBuilder<P, R> {
companion object {
@@ -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<BuildControllderAndIdeaModule, IAndroidProject>(androidVariant) {
class AndroidProjectModelBuilder(initializationParams: InitializeProjectParams) :
AbstractModelBuilder<BuildControllderAndIdeaModule, IAndroidProject>(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) {
@@ -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<GradleProject, IGradleProject>() {
class GradleProjectModelBuilder(initializationParams: InitializeProjectParams) :
AbstractModelBuilder<GradleProject, IGradleProject>(
initializationParams) {
@Throws(ModelBuilderException::class)
override fun build(param: GradleProject): IGradleProject {
@@ -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<JavaProjectModelBuilderParams, IJavaProject>() {
class JavaProjectModelBuilder(initializationParams: InitializeProjectParams) :
AbstractModelBuilder<JavaProjectModelBuilderParams, IJavaProject>(initializationParams) {
override fun build(param: JavaProjectModelBuilderParams): IJavaProject {
val compilerSettings = createCompilerSettings(param.project, param.module)
@@ -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<ModuleProjectModelBuilderParams, IModuleProject>(androidVariant) {
class ModuleProjectModelBuilder(initializationParams: InitializeProjectParams) :
AbstractModelBuilder<ModuleProjectModelBuilderParams, IModuleProject>(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))
}
}
@@ -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<ProjectConnectionAndAndroidVariant, IProject>("") {
class RootModelBuilder(initializationParams: InitializeProjectParams) :
AbstractModelBuilder<ProjectConnection, IProject>(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<ProjectConnectionAndAndroidVarian
val rootProject = if (rootProjectVersions != null) {
// Root project is an Android project
assertMinimumAgp(rootProjectVersions)
AndroidProjectModelBuilder(androidVariant).build(controller to rootModule)
AndroidProjectModelBuilder(initializationParams).build(controller to rootModule)
} else {
GradleProjectModelBuilder().build(rootModule.gradleProject)
GradleProjectModelBuilder(initializationParams).build(rootModule.gradleProject)
}
val projects = ideaModules.map { ideaModule ->
ModuleProjectModelBuilder(androidVariant).build(
ModuleProjectModelBuilder(initializationParams).build(
ModuleProjectModelBuilderParams(controller, ideaProject, ideaModule, modulePaths))
}
ProjectImpl(rootProject, projects)
return@action ProjectImpl(rootProject, projects)
}
finalizeLauncher(executor)
@@ -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<String, String>) {
constructor(base: ModuleProjectModelBuilderParams) : this(base.project, base.module, base.modulePaths)
}
typealias ProjectConnectionAndAndroidVariant = Pair<ProjectConnection, String>
constructor(base: ModuleProjectModelBuilderParams) : this(base.project, base.module,
base.modulePaths)
}
@@ -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()
}
@@ -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<String>
/**
* 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
@@ -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<String>,
val selectedVariantIndex: Int)
data class BuildVariantInfo(
val projectPath: String,
val buildVariants: List<String>,
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<String, BuildVariantInfo>.mapToSelectedVariants(): Map<String, String> {
return mapValues { it.value.selectedVariant }
}
@@ -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<String, String>) : Serializable {
companion object {
/**
* Default initialization params. This initializes the Android modules with default values.
*/
@JvmStatic
val DEFAULT = AndroidInitializationParams(emptyMap())
}
}
@@ -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()