@@ -16,6 +16,7 @@ import androidx.activity.ComponentActivity
1616import androidx.activity.SystemBarStyle
1717import androidx.activity.compose.setContent
1818import androidx.activity.enableEdgeToEdge
19+ import androidx.activity.result.contract.ActivityResultContracts
1920import androidx.compose.runtime.getValue
2021import androidx.compose.runtime.mutableStateOf
2122import androidx.compose.runtime.setValue
@@ -25,6 +26,8 @@ import androidx.lifecycle.lifecycleScope
2526import kotlinx.coroutines.Dispatchers
2627import kotlinx.coroutines.launch
2728import kotlinx.coroutines.withContext
29+ import org.randomcoder.udroid.audio.AudioConfiguration
30+ import org.randomcoder.udroid.audio.AudioConfigurationStore
2831import org.randomcoder.udroid.catalog.DistroCatalogRepository
2932import org.randomcoder.udroid.catalog.DistroCatalogState
3033import org.randomcoder.udroid.catalog.DistroVariant
@@ -111,6 +114,8 @@ class MainActivity : ComponentActivity() {
111114 mutableStateOf(DesktopConfiguration (null , compositingEnabled = false , touchScaleEnabled = true ))
112115 private var desktopScanLoading by mutableStateOf(false )
113116 private var desktopScanMessage by mutableStateOf<String ?>(null )
117+ private var audioConfiguration by mutableStateOf(AudioConfiguration ())
118+ private var audioConfigurationMessage by mutableStateOf<String ?>(null )
114119 private var linuxApplicationsState by
115120 mutableStateOf<LinuxApplicationsState >(LinuxApplicationsState .Loading )
116121 private var linuxApplicationMessage by mutableStateOf<String ?>(null )
@@ -119,6 +124,7 @@ class MainActivity : ComponentActivity() {
119124 private var pendingDesktopStart: PendingDesktopStart ? = null
120125 private var pendingShortcutRootfsName: String? = null
121126 private var pendingTerminalRootfsName: String? = null
127+ private var pendingMicrophoneRootfsName: String? = null
122128 private var linuxApplicationsLoadGeneration = 0L
123129 private var desktopScanGeneration = 0L
124130 private var ociCatalogueLoadGeneration = 0L
@@ -128,9 +134,33 @@ class MainActivity : ComponentActivity() {
128134 private var runtimeService by mutableStateOf<RuntimeSupervisorService ?>(null )
129135 private var runtimeServiceBound = false
130136 private val desktopConfigurationStore by lazy { DesktopConfigurationStore (this ) }
137+ private val audioConfigurationStore by lazy { AudioConfigurationStore (this ) }
131138 private val ociHubCatalogueRepository by lazy { OciHubCatalogRepository (this ) }
132139 private val ociHubTagRepository by lazy { OciHubTagRepository (this ) }
133140
141+ private val microphonePermissionLauncher =
142+ registerForActivityResult(ActivityResultContracts .RequestPermission ()) { granted ->
143+ val rootfsName = pendingMicrophoneRootfsName
144+ pendingMicrophoneRootfsName = null
145+ if (rootfsName == null ) return @registerForActivityResult
146+ if (granted) {
147+ val current = audioConfigurationStore.load(rootfsName)
148+ saveAudioConfiguration(
149+ rootfsName,
150+ current.copy(microphoneEnabled = true ),
151+ )
152+ } else {
153+ if (selectedSystemRootfsName == rootfsName) {
154+ audioConfiguration =
155+ audioConfigurationStore
156+ .load(rootfsName)
157+ .copy(microphoneEnabled = false )
158+ audioConfigurationMessage =
159+ " Microphone access was not granted. Linux input remains off."
160+ }
161+ }
162+ }
163+
134164 private val runtimeServiceConnection =
135165 object : ServiceConnection {
136166 override fun onServiceConnected (
@@ -193,6 +223,8 @@ class MainActivity : ComponentActivity() {
193223 desktopConfiguration = desktopConfiguration,
194224 desktopScanLoading = desktopScanLoading,
195225 desktopScanMessage = desktopScanMessage,
226+ audioConfiguration = audioConfiguration,
227+ audioConfigurationMessage = audioConfigurationMessage,
196228 linuxApplicationsState = linuxApplicationsState,
197229 linuxApplicationMessage = linuxApplicationMessage,
198230 showInstallTerminal = showInstallTerminal,
@@ -226,6 +258,8 @@ class MainActivity : ComponentActivity() {
226258 onSelectDesktopEnvironment = { selectDesktopEnvironment(it) },
227259 onCompositingChanged = { updateCompositing(it) },
228260 onTouchScaleChanged = { updateTouchScale(it) },
261+ onAudioOutputChanged = { updateAudioOutput(it) },
262+ onMicrophoneChanged = { updateMicrophone(it) },
229263 onStartDesktop = { startSelectedDesktop() },
230264 onStopDesktop = { runtimeService?.stopDesktop() },
231265 onRestartDesktop = { restartSelectedDesktop() },
@@ -606,6 +640,7 @@ class MainActivity : ComponentActivity() {
606640 if (installedRootfses.none { it.name == rootfsName }) return
607641 selectedSystemRootfsName = rootfsName
608642 rootfsMaintenanceMessage = null
643+ loadAudioConfiguration(rootfsName)
609644 selectDestination(UdroidDestination .SYSTEM )
610645 }
611646
@@ -679,6 +714,9 @@ class MainActivity : ComponentActivity() {
679714 runCatching { desktopConfigurationStore.remove(rootfsName) }
680715 .exceptionOrNull()
681716 ?.let { cleanupWarnings + = it.message ? : " desktop settings" }
717+ runCatching { audioConfigurationStore.remove(rootfsName) }
718+ .exceptionOrNull()
719+ ?.let { cleanupWarnings + = it.message ? : " audio settings" }
682720 runCatching {
683721 LinuxApplicationShortcutPublisher (this @MainActivity)
684722 .disableForRootfs(rootfsName)
@@ -850,6 +888,86 @@ class MainActivity : ComponentActivity() {
850888 )
851889 }
852890
891+ private fun loadAudioConfiguration (rootfsName : String ) {
892+ runCatching { audioConfigurationStore.load(rootfsName) }
893+ .onSuccess {
894+ audioConfiguration = it
895+ audioConfigurationMessage = null
896+ }.onFailure {
897+ audioConfiguration = AudioConfiguration ()
898+ audioConfigurationMessage = it.message ? : " Could not load audio settings"
899+ }
900+ }
901+
902+ private fun updateAudioOutput (enabled : Boolean ) {
903+ val rootfsName = selectedSystemRootfsName ? : return
904+ saveAudioConfiguration(
905+ rootfsName,
906+ audioConfiguration.copy(outputEnabled = enabled),
907+ )
908+ }
909+
910+ private fun updateMicrophone (enabled : Boolean ) {
911+ val rootfsName = selectedSystemRootfsName ? : return
912+ if (! enabled) {
913+ pendingMicrophoneRootfsName = null
914+ saveAudioConfiguration(
915+ rootfsName,
916+ audioConfiguration.copy(microphoneEnabled = false ),
917+ )
918+ return
919+ }
920+ if (
921+ ContextCompat .checkSelfPermission(this , Manifest .permission.RECORD_AUDIO ) ==
922+ PackageManager .PERMISSION_GRANTED
923+ ) {
924+ saveAudioConfiguration(
925+ rootfsName,
926+ audioConfiguration.copy(microphoneEnabled = true ),
927+ )
928+ return
929+ }
930+ pendingMicrophoneRootfsName = rootfsName
931+ audioConfigurationMessage = " Allow microphone access to send input to Linux."
932+ microphonePermissionLauncher.launch(Manifest .permission.RECORD_AUDIO )
933+ }
934+
935+ private fun saveAudioConfiguration (
936+ rootfsName : String ,
937+ configuration : AudioConfiguration ,
938+ ) {
939+ runCatching { audioConfigurationStore.save(rootfsName, configuration) }
940+ .onSuccess { saved ->
941+ val service = runtimeService
942+ val runningThisRootfs =
943+ service?.currentTerminalSession()?.isRunning == true &&
944+ service.currentTerminalSession()?.mSessionName == rootfsName
945+ if (selectedSystemRootfsName == rootfsName) {
946+ audioConfiguration = saved
947+ audioConfigurationMessage =
948+ if (runningThisRootfs) {
949+ " Applying audio settings to the running Linux system…"
950+ } else {
951+ " Audio settings will apply when this Linux system starts."
952+ }
953+ }
954+ if (runningThisRootfs) {
955+ service.applyAudioConfiguration(rootfsName, saved) { result ->
956+ if (selectedSystemRootfsName != rootfsName) return @applyAudioConfiguration
957+ audioConfigurationMessage =
958+ result.fold(
959+ onSuccess = { it.message },
960+ onFailure = {
961+ it.message ? : " Could not apply audio settings"
962+ },
963+ )
964+ }
965+ }
966+ }.onFailure {
967+ audioConfigurationMessage = it.message ? : " Could not save audio settings"
968+ }
969+ }
970+
853971 private fun saveDesktopConfiguration (
854972 rootfsName : String ,
855973 configuration : DesktopConfiguration ,
0 commit comments