Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/build
/build
/cache-test
/cache-test2
12 changes: 10 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ android {
applicationId = "org.scottishtecharmy.soundscape"
minSdk = 30
targetSdk = 35
versionCode = 172
versionName = "0.3.10"
versionCode = 174
versionName = "0.3.12"

// Maintaining this list means that we can exclude translations that aren't complete yet
resourceConfigurations.addAll(listOf(
Expand Down Expand Up @@ -367,3 +367,11 @@ dependencies {

testImplementation(libs.json)
}

dokka {
dokkaSourceSets.configureEach {
if (name == "main") {
suppress.set(true)
}
}
}
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

<application
android:name=".SoundscapeApplication"
Expand Down Expand Up @@ -108,7 +110,7 @@
android:stopWithTask="true"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="location|mediaPlayback"
android:foregroundServiceType="location|mediaPlayback|microphone"
android:permission="android.permission.FOREGROUND_SERVICE_LOCATION">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
Expand Down Expand Up @@ -146,6 +148,9 @@
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>

</manifest>
17 changes: 17 additions & 0 deletions app/src/main/java/org/scottishtecharmy/soundscape/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class MainActivity : AppCompatActivity() {
checkAndRequestLocationPermissions()
}

// Microphone permission for voice commands — best-effort; if denied, voice commands are silently skipped
private val micPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { /* no-op */ }

// we need location permission to be able to start the service
private val locationPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
Expand Down Expand Up @@ -164,6 +168,12 @@ class MainActivity : AppCompatActivity() {
hintsEnabled = preferences.getBoolean(HINTS_KEY, HINTS_DEFAULT)
)
}

MEDIA_CONTROLS_MODE_KEY -> {
val mode = preferences.getString(MEDIA_CONTROLS_MODE_KEY, MEDIA_CONTROLS_MODE_DEFAULT)!!
Log.e(TAG, "mediaControlsMode $mode")
soundscapeServiceConnection.soundscapeService?.updateMediaControls(mode)
}
}
}

Expand Down Expand Up @@ -746,6 +756,11 @@ class MainActivity : AppCompatActivity() {
Log.e(TAG, "startSoundscapeService")
val serviceIntent = Intent(this, SoundscapeService::class.java)
startForegroundService(serviceIntent)
// Request microphone permission for voice commands (best-effort)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
micPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}

companion object {
Expand Down Expand Up @@ -796,6 +811,8 @@ class MainActivity : AppCompatActivity() {
const val GEOCODER_MODE_KEY = "GeocoderMode"
const val LAST_SPLASH_RELEASE_DEFAULT = ""
const val LAST_SPLASH_RELEASE_KEY = "LastNewRelease"
const val MEDIA_CONTROLS_MODE_DEFAULT = "Original"
const val MEDIA_CONTROLS_MODE_KEY = "MediaControlsMode"

const val FIRST_LAUNCH_KEY = "FirstLaunch"
const val AUDIO_TOUR_SHOWN_KEY = "AudioTourShown"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.scottishtecharmy.soundscape.services.BeaconState
import org.scottishtecharmy.soundscape.services.RoutePlayerState
import org.scottishtecharmy.soundscape.services.SoundscapeBinder
import org.scottishtecharmy.soundscape.services.SoundscapeService
import org.scottishtecharmy.soundscape.services.mediacontrol.VoiceCommandState
import javax.inject.Inject

@ActivityRetainedScoped
Expand Down Expand Up @@ -48,13 +49,17 @@ class SoundscapeServiceConnection @Inject constructor() {
return soundscapeService?.gridStateFlow
}

fun getVoiceCommandStateFlow(): StateFlow<VoiceCommandState>? {
return soundscapeService?.voiceCommandStateFlow
}

fun setStreetPreviewMode(on : Boolean, location: LngLatAlt? = null) {
Log.d(TAG, "setStreetPreviewMode $on")
soundscapeService?.setStreetPreviewMode(on, location)
}

fun routeStart(routeId: Long) {
soundscapeService?.routeStart(routeId)
soundscapeService?.routeStartById(routeId)
}

fun startBeacon(location: LngLatAlt, name: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ class GeoEngine {
// So long as the AudioEngine is not already busy, run any auto callouts that we
// need. Auto Callouts use the direction of travel if there is one, otherwise
// falling back to use the phone direction.
if((!soundscapeService.isAudioEngineBusy() || streetPreview.running) && !autoCalloutDisabled) {
if((!soundscapeService.isAudioEngineBusy() || streetPreview.running) && !autoCalloutDisabled && !soundscapeService.menuActive) {
val callout =
autoCallout.updateLocation(
getCurrentUserGeometry(UserGeometry.HeadingMode.CourseAuto),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ fun Home(
goToAppSettings = goToAppSettings,
fullscreenMap = fullscreenMap,
permissionsRequired = permissionsRequired,
showMap = showMap
showMap = showMap,
voiceCommandListening = state.voiceCommandListening
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.scottishtecharmy.soundscape.screens.home.home

import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -160,14 +162,16 @@ fun HomeContent(
goToAppSettings: (Context) -> Unit,
fullscreenMap: MutableState<Boolean>,
permissionsRequired: Boolean,
showMap: Boolean) {
showMap: Boolean,
voiceCommandListening: Boolean = false) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var fetchingLocation by remember { mutableStateOf(false) }

Box(modifier = modifier.fillMaxSize()) {
Column(
verticalArrangement = Arrangement.spacedBy(spacing.small),
modifier = modifier
modifier = Modifier.fillMaxSize()
) {
if (streetPreviewState.enabled != StreetPreviewEnabled.OFF) {
StreetPreview(streetPreviewState, streetPreviewFunctions)
Expand Down Expand Up @@ -390,6 +394,25 @@ fun HomeContent(
}
}
}
if (voiceCommandListening) {
Box(
contentAlignment = Alignment.BottomCenter,
modifier = Modifier.fillMaxSize()
) {
Text(
text = stringResource(R.string.voice_cmd_listening),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.85f))
.padding(vertical = spacing.small)
.semantics { liveRegion = LiveRegionMode.Assertive },
textAlign = TextAlign.Center
)
}
}
} // end Box
}

@Preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ fun Settings(
"Offline"
)

val mediaControlsDescriptions = listOf(
stringResource(R.string.settings_media_controls_original),
stringResource(R.string.settings_media_controls_voice_command),
stringResource(R.string.settings_media_controls_audio_menu),
)
val mediaControlsValues = listOf(
"Original",
"VoiceControl",
"AudioMenu",
)

if (showConfirmationDialog.value) {
AlertDialog(
onDismissRequest = { showConfirmationDialog.value = false },
Expand Down Expand Up @@ -683,6 +694,42 @@ fun Settings(
}
}

// Media control section
item(key = "header_media_control") {
ExpandableSectionHeader(
title = stringResource(R.string.menu_media_controls),
expanded = expandedSection.value == "media_controls",
onToggle = { expandedSection.value = if (expandedSection.value == "media_controls") null else "media_controls" },
textColor = textColor
)
}
if (expandedSection.value == "media_controls") {
listPreference(
key = MainActivity.MEDIA_CONTROLS_MODE_KEY,
defaultValue = MainActivity.MEDIA_CONTROLS_MODE_DEFAULT,
values = mediaControlsValues,
modifier = expandedSectionModifier,
title = {
SettingDetails(
R.string.settings_section_media_controls,
R.string.settings_section_media_controls_description,
textColor
)
},
item = { value, currentValue, onClick ->
ListPreferenceItem(mediaControlsDescriptions[mediaControlsValues.indexOf(value)], value, currentValue, onClick, mediaControlsValues.indexOf(value), mediaControlsValues.size)
},
summary = {
Text(
text = mediaControlsDescriptions[mediaControlsValues.indexOf(it)],
color = textColor,
style = MaterialTheme.typography.bodyLarge
)
},
)
}


// Debug Section
item(key = "header_debug") {
ExpandableSectionHeader(
Expand Down Expand Up @@ -800,6 +847,11 @@ fun SettingsPreviewLanguage() {
}
@Preview
@Composable
fun SettingsPreviewMediaControls() {
SettingsPreview("media_controls")
}
@Preview
@Composable
fun SettingsPreviewDebug() {
SettingsPreview("debug")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Mic
import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
Expand Down Expand Up @@ -51,12 +52,14 @@ fun NavigatingScreen(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
Manifest.permission.POST_NOTIFICATIONS
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.RECORD_AUDIO
)
} else {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
Manifest.permission.RECORD_AUDIO
)
}
val multiplePermissionResultLauncher = rememberLauncherForActivityResult(
Expand Down Expand Up @@ -135,7 +138,7 @@ fun Navigating(
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(spacing.medium))
Spacer(modifier = Modifier.width(spacing.extraSmall))
Column(
modifier = Modifier.semantics(mergeDescendants = true) {},
) {
Expand Down Expand Up @@ -182,7 +185,33 @@ fun Navigating(
)
}
}

}
Row(
modifier = Modifier
.mediumPadding()
.fillMaxWidth(),
)
{
Icon(
Icons.Rounded.Mic,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(spacing.extraSmall))
Column(
modifier = Modifier.semantics(mergeDescendants = true) {},
) {
Text(
text = stringResource(R.string.first_launch_permissions_record_audio),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = stringResource(R.string.first_launch_permissions_required_for_voice_control),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}

Expand Down

This file was deleted.

Loading