mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 15:56:48 +08:00
feat: add support for selecting build variants in project (closes #223)
This commit is contained in:
@@ -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
|
||||
|
||||
+46
-46
@@ -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)
|
||||
|
||||
+205
-18
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-10
@@ -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(
|
||||
|
||||
+3
-2
@@ -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.
|
||||
|
||||
+4
-1
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+7
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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())
|
||||
|
||||
+4
-2
@@ -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 {
|
||||
|
||||
|
||||
+11
-8
@@ -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) {
|
||||
|
||||
+4
-1
@@ -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 {
|
||||
|
||||
+3
-2
@@ -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)
|
||||
|
||||
+5
-4
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
+17
-8
@@ -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)
|
||||
|
||||
+4
-3
@@ -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)
|
||||
}
|
||||
+2
-3
@@ -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()
|
||||
}
|
||||
|
||||
+11
@@ -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
|
||||
|
||||
+31
-4
@@ -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 }
|
||||
}
|
||||
|
||||
+27
-4
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user