@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
44import androidx.lifecycle.viewModelScope
55import com.itsaky.androidide.lookup.Lookup
66import com.itsaky.androidide.models.ApkMetadata
7+ import com.itsaky.androidide.models.installTaskRequestsIn
78import com.itsaky.androidide.project.AndroidModels
89import com.itsaky.androidide.projects.IProjectManager
910import com.itsaky.androidide.projects.api.AndroidModule
@@ -46,37 +47,14 @@ class BuildViewModel : ViewModel() {
4647 gradleArgs : List <String > = emptyList(),
4748 onTerminalState : ((BuildState ) -> Unit )? = null,
4849 ) {
49- // Claim the slot before the coroutine is scheduled, and in one step: a check here and a set
50- // inside the launched block let two callers both read a free state and both reach
51- // executeTasks, running duplicate build-and-install flows.
52- while (true ) {
53- val current = _buildState .value
54- if (current is BuildState .InProgress ) {
55- log.warn(" Build is already in progress. Ignoring new request." )
56- onTerminalState?.invoke(BuildState .Error (" A build is already in progress." ))
57- return
58- }
59- if (_buildState .compareAndSet(current, BuildState .InProgress )) {
60- break
61- }
62- }
50+ if (! claimBuildSlot(onTerminalState)) return
6351
6452 viewModelScope.launch {
65- var reported = false
66-
67- // Publishes a terminal state and notifies the caller once, from the one place that
68- // knows the run is over. Called only on the main dispatcher, so the flag needs no lock.
69- fun finish (state : BuildState ) {
70- _buildState .value = state
71- if (! reported) {
72- reported = true
73- onTerminalState?.invoke(state)
74- }
75- }
53+ val reporter = RunReporter (onTerminalState)
7654
7755 val buildService = Lookup .getDefault().lookup(BuildService .KEY_BUILD_SERVICE )
7856 if (buildService == null ) {
79- finish(BuildState .Error (" Build service not found." ))
57+ reporter. finish(BuildState .Error (" Build service not found." ))
8058 return @launch
8159 }
8260
@@ -118,10 +96,10 @@ class BuildViewModel : ViewModel() {
11896 val cgpFile =
11997 withContext(Dispatchers .IO ) { findPluginCgpFile(projectRoot, variant) }
12098 if (cgpFile != null ) {
121- finish(BuildState .AwaitingPluginInstall (cgpFile))
99+ reporter. finish(BuildState .AwaitingPluginInstall (cgpFile))
122100 } else {
123101 log.warn(" Plugin built successfully but .cgp file not found" )
124- finish(
102+ reporter. finish(
125103 BuildState .Error (" Plugin built but output file (.cgp) not found in build/plugin" ),
126104 )
127105 }
@@ -140,7 +118,7 @@ class BuildViewModel : ViewModel() {
140118 throw RuntimeException (" APK file specified does not exist: $apkFile " )
141119 }
142120
143- finish(
121+ reporter. finish(
144122 BuildState .AwaitingInstall (
145123 apkFile,
146124 launchInDebugMode,
@@ -150,15 +128,103 @@ class BuildViewModel : ViewModel() {
150128 } catch (e: Exception ) {
151129 if (e is CancellationException ) {
152130 log.info(" Build was cancelled by the user." )
153- finish(BuildState .Idle )
131+ reporter. finish(BuildState .Idle )
154132 } else {
155133 log.error(" Quick Run failed." , e)
156- finish(BuildState .Error (e.message ? : " An unknown error occurred." ))
134+ reporter.finish(BuildState .Error (e.message ? : " An unknown error occurred." ))
135+ }
136+ }
137+ }
138+ }
139+
140+ private inner class RunReporter (
141+ private val onTerminalState : ((BuildState ) -> Unit )? ,
142+ ) {
143+ private var reported = false
144+
145+ fun finish (state : BuildState ) {
146+ _buildState .value = state
147+ if (! reported) {
148+ reported = true
149+ onTerminalState?.invoke(state)
150+ }
151+ }
152+ }
153+
154+ private fun claimBuildSlot (onTerminalState : ((BuildState ) -> Unit )? ): Boolean {
155+ while (true ) {
156+ val current = _buildState .value
157+ if (current is BuildState .InProgress ) {
158+ log.warn(" Build is already in progress. Ignoring new request." )
159+ onTerminalState?.invoke(BuildState .Error (" A build is already in progress." ))
160+ return false
161+ }
162+ if (_buildState .compareAndSet(current, BuildState .InProgress )) return true
163+ }
164+ }
165+
166+ fun runTasks (
167+ tasks : List <String >,
168+ onTerminalState : ((BuildState ) -> Unit )? = null,
169+ ) {
170+ if (! claimBuildSlot(onTerminalState)) return
171+ viewModelScope.launch {
172+ val reporter = RunReporter (onTerminalState)
173+ val buildService = Lookup .getDefault().lookup(BuildService .KEY_BUILD_SERVICE )
174+ if (buildService == null ) {
175+ reporter.finish(BuildState .Error (" Build service not found." ))
176+ return @launch
177+ }
178+ try {
179+ val result = withContext(Dispatchers .IO ) { buildService.executeTasks(tasks) }.await()
180+ if (! result.isSuccessful) {
181+ throw RuntimeException (" Task execution failed: ${result.failure} " )
182+ }
183+ val apkFile = withContext(Dispatchers .IO ) { apkForInstallRequests(tasks) }
184+ if (apkFile == null ) {
185+ reporter.finish(BuildState .Idle )
186+ } else {
187+ reporter.finish(BuildState .AwaitingInstall (apkFile, launchInDebugMode = false ))
188+ }
189+ } catch (e: Exception ) {
190+ if (e is CancellationException ) {
191+ log.info(" Build was cancelled by the user." )
192+ reporter.finish(BuildState .Idle )
193+ } else {
194+ log.error(" Task run failed." , e)
195+ reporter.finish(BuildState .Error (e.message ? : " An unknown error occurred." ))
157196 }
158197 }
159198 }
160199 }
161200
201+ private fun apkForInstallRequests (tasks : List <String >): File ? {
202+ val requests = installTaskRequestsIn(tasks)
203+ if (requests.isEmpty()) return null
204+ if (requests.size > 1 ) {
205+ log.warn(" Several install tasks were requested; only {} is installed." , requests.first())
206+ }
207+ val request = requests.first()
208+ val variant =
209+ IProjectManager
210+ .getInstance()
211+ .getAndroidAppModules()
212+ .filter { request.modulePath == null || it.path == request.modulePath }
213+ .firstNotNullOfOrNull { module ->
214+ module.variantList.firstOrNull { it.mainArtifact.assembleTaskName == request.assembleTaskName }
215+ }
216+ ? : throw RuntimeException (
217+ " No Android application variant is assembled by '${request.assembleTaskName} ' in ${request.modulePath ? : " the project" } ." ,
218+ )
219+ val apkFile =
220+ ApkMetadata .findApkFile(variant.mainArtifact.assembleTaskOutputListingFile)
221+ ? : throw RuntimeException (" No APK found in output listing file." )
222+ if (! apkFile.exists()) {
223+ throw RuntimeException (" APK file specified does not exist: $apkFile " )
224+ }
225+ return apkFile
226+ }
227+
162228 /* * Call this after the installation attempt to reset the state. */
163229 fun installationAttempted () {
164230 if (_buildState .value is BuildState .AwaitingInstall ) {
0 commit comments