yacwc
This commit is contained in:
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
6
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
6
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@@ -4,6 +4,14 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="wear">
|
<SelectionState runConfigName="wear">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2024-09-04T15:12:51.439753961Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="LocalEmulator" identifier="path=/home/isp/.config/.android/avd/Wear_OS_Large_Round_API_34.avd" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="mobile">
|
<SelectionState runConfigName="mobile">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
|||||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.2"
|
agp = "8.6.0"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.13.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
@@ -18,6 +18,8 @@ activityCompose = "1.9.1"
|
|||||||
coreSplashscreen = "1.0.1"
|
coreSplashscreen = "1.0.1"
|
||||||
composeNavigation = "1.4.0-rc01"
|
composeNavigation = "1.4.0-rc01"
|
||||||
media3Common = "1.4.0"
|
media3Common = "1.4.0"
|
||||||
|
composeMaterial3 = "1.0.0-alpha23"
|
||||||
|
workRuntimeKtx = "2.9.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -43,6 +45,8 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp
|
|||||||
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
|
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
|
||||||
androidx-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "composeNavigation" }
|
androidx-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "composeNavigation" }
|
||||||
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
||||||
|
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
|
||||||
|
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ dependencies {
|
|||||||
implementation("com.google.android.horologist:horologist-compose-material:0.6.8")
|
implementation("com.google.android.horologist:horologist-compose-material:0.6.8")
|
||||||
implementation("com.google.android.horologist:horologist-media-ui:0.6.8")
|
implementation("com.google.android.horologist:horologist-media-ui:0.6.8")
|
||||||
implementation("com.google.android.horologist:horologist-media-data:0.6.8")
|
implementation("com.google.android.horologist:horologist-media-data:0.6.8")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1")
|
||||||
implementation("androidx.media3:media3-exoplayer:1.4.0")
|
implementation("androidx.media3:media3-exoplayer:1.4.0")
|
||||||
implementation(libs.play.services.wearable)
|
implementation(libs.play.services.wearable)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
@@ -67,7 +67,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
implementation(libs.androidx.compose.navigation)
|
implementation(libs.androidx.compose.navigation)
|
||||||
implementation(libs.androidx.media3.common)
|
implementation(libs.androidx.media3.common)
|
||||||
// androidTestImplementation(platform(libs.androidx.compose.bom))
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.work.runtime.ktx) // androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
// androidTestImplementation(libs.androidx.ui.test.junit4)
|
// androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
// debugImplementation(libs.androidx.ui.tooling)
|
// debugImplementation(libs.androidx.ui.tooling)
|
||||||
// debugImplementation(libs.androidx.ui.test.manifest)
|
// debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
|
|||||||
@@ -1,110 +1,68 @@
|
|||||||
package com.birdsounds.identify.presentation
|
package com.birdsounds.identify.presentation
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Mic
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import com.birdsounds.identify.R
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.wear.compose.material.Button
|
import androidx.wear.compose.material.CompactChip
|
||||||
import androidx.wear.compose.material.CircularProgressIndicator
|
import androidx.wear.compose.material.MaterialTheme
|
||||||
import androidx.wear.compose.material.Icon
|
import androidx.wear.compose.material.Text
|
||||||
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The component responsible for drawing the main 3 controls, with their expanded and minimized
|
|
||||||
* states.
|
|
||||||
*
|
|
||||||
* The state for this class is driven by a [ControlDashboardUiState], which contains a
|
|
||||||
* [ControlDashboardButtonUiState] for each of the three buttons.
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ControlDashboard(
|
fun ControlDashboard(controlDashboardUiState: ControlDashboardUiState, onMicClicked: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
controlDashboardUiState: ControlDashboardUiState,
|
Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) {
|
||||||
onMicClicked: () -> Unit,
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
recordingProgress: Float,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
ControlDashboardButton(buttonState = controlDashboardUiState.micState,
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
ControlDashboardButton(
|
|
||||||
buttonState = controlDashboardUiState.micState,
|
|
||||||
onClick = onMicClicked,
|
onClick = onMicClicked,
|
||||||
imageVector = Icons.Filled.Mic,
|
labelText = if (controlDashboardUiState.micState.expanded) {
|
||||||
contentDescription = if (controlDashboardUiState.micState.expanded) {
|
"Stop"
|
||||||
stringResource(id = R.string.stop_recording)
|
|
||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.record)
|
"Start"
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A single control dashboard button
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ControlDashboardButton(
|
private fun ControlDashboardButton(buttonState: ControlDashboardButtonUiState,
|
||||||
buttonState: ControlDashboardButtonUiState,
|
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
imageVector: ImageVector,
|
labelText: String,
|
||||||
contentDescription: String,
|
modifier: Modifier = Modifier) {
|
||||||
modifier: Modifier = Modifier
|
CompactChip(
|
||||||
) {
|
modifier = Modifier,
|
||||||
Button(
|
onClick = onClick ,
|
||||||
modifier = modifier,
|
enabled = true,
|
||||||
enabled = buttonState.enabled && buttonState.visible,
|
contentPadding = PaddingValues(5.dp, 1.dp, 5.dp, 1.dp),
|
||||||
onClick = onClick
|
shape = MaterialTheme.shapes.small,
|
||||||
) {
|
label = {
|
||||||
Icon(
|
Text(
|
||||||
imageVector = imageVector,
|
text = labelText
|
||||||
contentDescription = contentDescription
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The state for a single [ControlDashboardButton].
|
|
||||||
*/
|
|
||||||
data class ControlDashboardButtonUiState(
|
|
||||||
val expanded: Boolean,
|
|
||||||
val enabled: Boolean,
|
|
||||||
val visible: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The state for a [ControlDashboard].
|
// Button(modifier = modifier, enabled = buttonState.enabled && buttonState.visible, onClick = onClick) {
|
||||||
*/
|
// Text(contentDescription);
|
||||||
data class ControlDashboardUiState(
|
//// Icon(imageVector = imageVector, contentDescription = contentDescription)
|
||||||
val micState: ControlDashboardButtonUiState,
|
// }
|
||||||
val playState: ControlDashboardButtonUiState
|
//}
|
||||||
) {
|
|
||||||
init {
|
|
||||||
// Check that at most one of the buttons is expanded
|
data class ControlDashboardButtonUiState(val expanded: Boolean, val enabled: Boolean, val visible: Boolean)
|
||||||
require(
|
|
||||||
listOf(
|
|
||||||
micState.expanded,
|
data class ControlDashboardUiState(val micState: ControlDashboardButtonUiState) {
|
||||||
playState.expanded
|
init { // Check that at most one of the buttons is expanded
|
||||||
).count { it } <= 1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,74 +13,69 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.google.android.horologist.audio.ui.VolumeViewModel
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import com.google.android.horologist.compose.layout.AppScaffold
|
|
||||||
import com.google.android.horologist.compose.layout.ScreenScaffold
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.wear.compose.material.MaterialTheme
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.wear.compose.material.Text
|
|
||||||
import androidx.wear.compose.material.TimeText
|
|
||||||
import androidx.wear.compose.material.dialog.Confirmation
|
|
||||||
import androidx.wear.compose.navigation.SwipeDismissableNavHost
|
import androidx.wear.compose.navigation.SwipeDismissableNavHost
|
||||||
import androidx.wear.compose.navigation.composable
|
import androidx.wear.compose.navigation.composable
|
||||||
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
|
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
|
||||||
import androidx.wear.tooling.preview.devices.WearDevices
|
|
||||||
import com.birdsounds.identify.R
|
|
||||||
import com.birdsounds.identify.presentation.theme.IdentifyTheme
|
import com.birdsounds.identify.presentation.theme.IdentifyTheme
|
||||||
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
||||||
import com.google.android.horologist.audio.ui.VolumeScreen
|
import com.google.android.horologist.audio.ui.VolumeViewModel
|
||||||
import com.google.android.horologist.compose.material.AlertContent
|
import com.google.android.horologist.compose.layout.AppScaffold
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
val flow_stream = MutableStateFlow<String>("")
|
||||||
|
|
||||||
|
val Any.TAG: String
|
||||||
|
get() {
|
||||||
|
val tag = javaClass.simpleName
|
||||||
|
return if (tag.length <= 23) tag else tag.substring(0, 23)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setTheme(android.R.style.Theme_DeviceDefault)
|
setTheme(android.R.style.Theme_DeviceDefault)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
WearApp("Android")
|
WearApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalHorologistApi::class)
|
@OptIn(ExperimentalHorologistApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun WearApp(greetingName: String) {
|
fun WearApp() {
|
||||||
|
|
||||||
IdentifyTheme {
|
IdentifyTheme {
|
||||||
lateinit var requestPermissionLauncher: ManagedActivityResultLauncher<String, Boolean>
|
lateinit var requestPermissionLauncher: ManagedActivityResultLauncher<String, Boolean>
|
||||||
|
|
||||||
|
// val job = launch {
|
||||||
|
// flow_stream.collect {
|
||||||
|
// print("$it ")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context.findActivity()
|
val activity = context.findActivity()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
val volumeViewModel: VolumeViewModel = viewModel(factory = VolumeViewModel.Factory)
|
val volumeViewModel: VolumeViewModel = viewModel(factory = VolumeViewModel.Factory)
|
||||||
|
|
||||||
val navController = rememberSwipeDismissableNavController()
|
val navController = rememberSwipeDismissableNavController()
|
||||||
|
|
||||||
val mainState = remember(activity) {
|
val mainState = remember(activity) {
|
||||||
@@ -93,25 +88,20 @@ fun WearApp(greetingName: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) {
|
requestPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) {
|
||||||
// We ignore the direct result here, since we're going to check anyway.
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
mainState.permissionResultReturned()
|
mainState.permissionResultReturned()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||||
|
|
||||||
AppScaffold {
|
AppScaffold {
|
||||||
// Notify the state holder whenever we become stopped to reset the state
|
|
||||||
DisposableEffect(mainState, scope, lifecycleOwner) {
|
DisposableEffect(mainState, scope, lifecycleOwner) {
|
||||||
val lifecycleObserver = object : DefaultLifecycleObserver {
|
val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
super.onStop(owner)
|
super.onStop(owner)
|
||||||
scope.launch { mainState.onStopped() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -120,63 +110,19 @@ fun WearApp(greetingName: String) {
|
|||||||
}
|
}
|
||||||
SwipeDismissableNavHost(navController = navController, startDestination = "speaker") {
|
SwipeDismissableNavHost(navController = navController, startDestination = "speaker") {
|
||||||
composable("speaker") {
|
composable("speaker") {
|
||||||
SpeakerRecordingScreen(
|
StartRecordingScreen(
|
||||||
playbackState = mainState.playbackState,
|
appState = mainState.appState,
|
||||||
isPermissionDenied = mainState.isPermissionDenied,
|
isPermissionDenied = mainState.isPermissionDenied,
|
||||||
recordingProgress = mainState.recordingProgress,
|
|
||||||
onMicClicked = {
|
onMicClicked = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
mainState.onMicClicked()
|
mainState.onMicClicked()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (mainState.showPermissionRationale) {
|
|
||||||
AlertContent(
|
|
||||||
onOk = {
|
|
||||||
requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
|
||||||
mainState.showPermissionRationale = false
|
|
||||||
},
|
},
|
||||||
onCancel = {
|
|
||||||
mainState.showPermissionRationale = false
|
|
||||||
},
|
|
||||||
title = stringResource(
|
|
||||||
id = R.string.rationale_for_microphone_permission
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainState.showSpeakerNotSupported) {
|
|
||||||
Confirmation(
|
|
||||||
onTimeout = { mainState.showSpeakerNotSupported = false }
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = R.string.no_speaker_supported))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(greetingName: String) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colors.primary,
|
|
||||||
text = stringResource(R.string.hello_world, greetingName)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
|
|
||||||
@Composable
|
|
||||||
fun DefaultPreview() {
|
|
||||||
WearApp("Preview Android")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tailrec fun Context.findActivity(): Activity =
|
tailrec fun Context.findActivity(): Activity =
|
||||||
|
|||||||
@@ -3,185 +3,105 @@ package com.birdsounds.identify.presentation
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.media.AudioDeviceInfo
|
import android.util.Log
|
||||||
import android.media.AudioManager
|
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.compose.foundation.MutatorMutex
|
import androidx.compose.foundation.MutatorMutex
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import java.time.Duration
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
class MainState(
|
class MainState(private val activity: Activity, private val requestPermission: () -> Unit) {
|
||||||
private val activity: Activity,
|
|
||||||
private val requestPermission: () -> Unit
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* The [MutatorMutex] that guards the playback state of the app.
|
|
||||||
*
|
|
||||||
* Due to being a [MutatorMutex], this automatically handles cleanup of any ongoing asynchronous
|
|
||||||
* work, like playing music or recording, ensuring that only one operation is occurring at a
|
|
||||||
* time.
|
|
||||||
*
|
|
||||||
* For example, if the user is currently recording, and they hit the mic button again, the
|
|
||||||
* second [onMicClicked] will cancel the previous [onMicClicked] that was doing the recording,
|
|
||||||
* waiting for everything to be cleaned up, before running its own code.
|
|
||||||
*/
|
|
||||||
private val playbackStateMutatorMutex = MutatorMutex()
|
private val playbackStateMutatorMutex = MutatorMutex()
|
||||||
|
var appState by mutableStateOf<AppState>(AppState.Ready)
|
||||||
/**
|
|
||||||
* The primary playback state.
|
|
||||||
*/
|
|
||||||
var playbackState by mutableStateOf<PlaybackState>(PlaybackState.Ready)
|
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
|
||||||
* The progress of an ongoing recording.
|
|
||||||
*
|
|
||||||
* Note that this value can be read even when recording is not occurring, in which case it
|
|
||||||
* corresponds to the last known value of recording progress (or 0), where that value is useful
|
|
||||||
* for animations.
|
|
||||||
*/
|
|
||||||
var recordingProgress by mutableStateOf(0f)
|
var recordingProgress by mutableStateOf(0f)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
|
||||||
* `true` if we know the user has denied the record audio permission.
|
|
||||||
*/
|
|
||||||
var isPermissionDenied by mutableStateOf(false)
|
var isPermissionDenied by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
|
||||||
* `true` if we the permission rationale should be shown.
|
|
||||||
*/
|
|
||||||
var showPermissionRationale by mutableStateOf(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `true` if we the speaker not supported rationale should be shown.
|
|
||||||
*/
|
|
||||||
var showSpeakerNotSupported by mutableStateOf(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The [SoundRecorder] for recording and playing audio captured on-device.
|
|
||||||
*/
|
|
||||||
private val soundRecorder = SoundRecorder(activity, "audiorecord.opus")
|
private val soundRecorder = SoundRecorder(activity, "audiorecord.opus")
|
||||||
|
|
||||||
suspend fun onStopped() {
|
|
||||||
playbackStateMutatorMutex.mutate {
|
|
||||||
playbackState = PlaybackState.Ready
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onMicClicked() {
|
suspend fun onMicClicked() {
|
||||||
playbackStateMutatorMutex.mutate {
|
playbackStateMutatorMutex.mutate {
|
||||||
when (playbackState) {
|
when (appState) {
|
||||||
is PlaybackState.Ready,
|
is AppState.Ready -> when {
|
||||||
PlaybackState.PlayingVoice ->
|
(ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) -> {
|
||||||
// If we weren't recording, check our permission to start recording.
|
Log.e(TAG, "Permissions granted, continuing to record");
|
||||||
when {
|
appState = AppState.Recording
|
||||||
ContextCompat.checkSelfPermission(
|
record(soundRecorder = soundRecorder, setProgress = { progress -> recordingProgress = progress })
|
||||||
activity,
|
|
||||||
Manifest.permission.RECORD_AUDIO
|
|
||||||
) == PackageManager.PERMISSION_GRANTED -> {
|
|
||||||
// We have the permission, we can start recording now
|
|
||||||
playbackState = PlaybackState.Recording
|
|
||||||
record(
|
|
||||||
soundRecorder = soundRecorder,
|
|
||||||
setProgress = { progress ->
|
|
||||||
recordingProgress = progress
|
|
||||||
}
|
|
||||||
)
|
|
||||||
playbackState = PlaybackState.Ready
|
|
||||||
}
|
|
||||||
activity.shouldShowRequestPermissionRationale(
|
|
||||||
Manifest.permission.RECORD_AUDIO
|
|
||||||
) -> {
|
|
||||||
// If we should show the rationale prior to requesting the permission,
|
|
||||||
// send that event
|
|
||||||
showPermissionRationale = true
|
|
||||||
playbackState = PlaybackState.Ready
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// Request the permission
|
Log.e(TAG, "Requesting permissions");
|
||||||
requestPermission()
|
requestPermission()
|
||||||
playbackState = PlaybackState.Ready
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we were already recording, transition back to ready
|
|
||||||
PlaybackState.Recording -> {
|
AppState.Recording -> {
|
||||||
playbackState = PlaybackState.Ready
|
appState = AppState.Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun permissionResultReturned() {
|
suspend fun permissionResultReturned() {
|
||||||
playbackStateMutatorMutex.mutate {
|
playbackStateMutatorMutex.mutate {
|
||||||
// Check if the user granted the permission
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||||
if (
|
appState = AppState.Recording
|
||||||
ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) ==
|
record(soundRecorder = soundRecorder, setProgress = { progress ->
|
||||||
PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
// The user granted the permission, continue on to start recording
|
|
||||||
playbackState = PlaybackState.Recording
|
|
||||||
record(
|
|
||||||
soundRecorder = soundRecorder,
|
|
||||||
setProgress = { progress ->
|
|
||||||
recordingProgress = progress
|
recordingProgress = progress
|
||||||
}
|
})
|
||||||
)
|
appState = AppState.Ready
|
||||||
playbackState = PlaybackState.Ready
|
|
||||||
} else {
|
} else {
|
||||||
// We have confirmation now that the user denied the permission
|
|
||||||
isPermissionDenied = true
|
isPermissionDenied = true
|
||||||
playbackState = PlaybackState.Ready
|
appState = AppState.Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
sealed class AppState {
|
||||||
* The three playback states of the application.
|
object Ready : AppState()
|
||||||
*/
|
object Recording : AppState()
|
||||||
sealed class PlaybackState {
|
|
||||||
object Ready : PlaybackState()
|
|
||||||
object PlayingVoice : PlaybackState()
|
|
||||||
object Recording : PlaybackState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper function to record, updating the progress state while recording.
|
|
||||||
*
|
|
||||||
* This requires the [Manifest.permission.RECORD_AUDIO] permission to run.
|
|
||||||
*/
|
|
||||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||||
private suspend fun record(
|
private suspend fun record(soundRecorder: SoundRecorder,
|
||||||
soundRecorder: SoundRecorder,
|
|
||||||
setProgress: (progress: Float) -> Unit,
|
setProgress: (progress: Float) -> Unit,
|
||||||
maxRecordingDuration: Duration = Duration.ofSeconds(10),
|
maxRecordingDuration: Duration = Duration.ofSeconds(10),
|
||||||
numberTicks: Int = 10
|
numberTicks: Int = 10) {
|
||||||
) {
|
|
||||||
coroutineScope {
|
|
||||||
// Kick off a parallel job to record
|
|
||||||
val recordingJob = launch { soundRecorder.record() }
|
|
||||||
|
|
||||||
val delayPerTickMs = maxRecordingDuration.toMillis() / numberTicks
|
coroutineScope { // Kick off a parallel job to
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
repeat(numberTicks) { index ->
|
Log.e(TAG, "Mock recording"); // val recordingJob = launch { soundRecorder.record() }
|
||||||
setProgress(index.toFloat() / numberTicks)
|
val ByteFlow: Flow<String> = flow{
|
||||||
delay(startTime + delayPerTickMs * (index + 1) - System.currentTimeMillis())
|
for (i in 1..3) {
|
||||||
|
var string_send = LocalDateTime.now().toString()
|
||||||
|
emit(string_send);
|
||||||
|
delay(250);
|
||||||
|
Log.e(TAG, "Emitting " + string_send)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update the progress to be complete
|
|
||||||
setProgress(1f)
|
|
||||||
|
|
||||||
// Stop recording
|
//
|
||||||
recordingJob.cancel()
|
// val delayPerTickMs = maxRecordingDuration.toMillis() / numberTicks
|
||||||
|
// val startTime = System.currentTimeMillis()
|
||||||
|
//
|
||||||
|
// repeat(numberTicks) { index ->
|
||||||
|
// setProgress(index.toFloat() / numberTicks)
|
||||||
|
// delay(startTime + delayPerTickMs * (index + 1) - System.currentTimeMillis())
|
||||||
|
// } // Update the progress to be complete
|
||||||
|
// setProgress(1f)
|
||||||
|
//
|
||||||
|
// // Stop recording
|
||||||
|
// recordingJob.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.birdsounds.identify.presentation
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.google.android.gms.wearable.Wearable
|
||||||
|
|
||||||
|
class MessageSendRecv(private val context: Context)
|
||||||
|
{
|
||||||
|
//MainState.
|
||||||
|
}
|
||||||
@@ -3,11 +3,17 @@ package com.birdsounds.identify.presentation
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.AudioRecord
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
|
import com.google.android.gms.wearable.Wearable
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper class to provide methods to record audio input from the MIC to the internal storage.
|
* A helper class to provide methods to record audio input from the MIC to the internal storage.
|
||||||
@@ -17,27 +23,25 @@ class SoundRecorder(
|
|||||||
outputFileName: String
|
outputFileName: String
|
||||||
) {
|
) {
|
||||||
private val audioFile = File(context.filesDir, outputFileName)
|
private val audioFile = File(context.filesDir, outputFileName)
|
||||||
|
|
||||||
private var state = State.IDLE
|
private var state = State.IDLE
|
||||||
|
|
||||||
private enum class State {
|
private enum class State {
|
||||||
IDLE, RECORDING
|
IDLE, RECORDING
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Records from the microphone.
|
|
||||||
*
|
|
||||||
* This method is cancellable, and cancelling it will stop recording.
|
|
||||||
*/
|
|
||||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||||
suspend fun record() {
|
suspend fun record() {
|
||||||
if (state != State.IDLE) {
|
|
||||||
Log.w(TAG, "Requesting to start recording while state was not IDLE")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
suspendCancellableCoroutine<Unit> { cont ->
|
suspendCancellableCoroutine<Unit> { cont ->
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|
||||||
val mediaRecorder = MediaRecorder().apply {
|
val mediaRecorder = MediaRecorder().apply {
|
||||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||||
setOutputFormat(MediaRecorder.OutputFormat.OGG)
|
setOutputFormat(MediaRecorder.OutputFormat.OGG)
|
||||||
@@ -58,7 +62,7 @@ class SoundRecorder(
|
|||||||
|
|
||||||
mediaRecorder.prepare()
|
mediaRecorder.prepare()
|
||||||
mediaRecorder.start()
|
mediaRecorder.start()
|
||||||
|
Log.e("com.birdsounds.identify","Hey I'm recording")
|
||||||
state = State.RECORDING
|
state = State.RECORDING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.birdsounds.identify.presentation
|
|
||||||
|
|
||||||
import androidx.activity.compose.ReportDrawnAfter
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
|
||||||
import com.google.android.horologist.audio.ui.VolumeViewModel
|
|
||||||
import com.google.android.horologist.audio.ui.components.actions.SetVolumeButton
|
|
||||||
import com.google.android.horologist.media.ui.components.PodcastControlButtons
|
|
||||||
import com.google.android.horologist.media.ui.screens.player.DefaultMediaInfoDisplay
|
|
||||||
import com.google.android.horologist.media.ui.screens.player.PlayerScreen
|
|
||||||
import com.google.android.horologist.media.ui.state.PlayerUiController
|
|
||||||
import com.google.android.horologist.media.ui.state.PlayerUiState
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
|
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
|
||||||
@OptIn(ExperimentalHorologistApi::class)
|
|
||||||
@Composable
|
|
||||||
fun SpeakerPlayerScreen(
|
|
||||||
onVolumeClick: () -> Unit,
|
|
||||||
volumeViewModel: VolumeViewModel = viewModel(factory = VolumeViewModel.Factory),
|
|
||||||
playerViewModel: SpeakerPlayerViewModel = viewModel(factory = SpeakerPlayerViewModel.Factory),
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val volumeUiState by volumeViewModel.volumeUiState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
PlayerScreen(
|
|
||||||
modifier = modifier,
|
|
||||||
background = {},
|
|
||||||
playerViewModel = playerViewModel,
|
|
||||||
volumeViewModel = volumeViewModel,
|
|
||||||
mediaDisplay = { playerUiState ->
|
|
||||||
DefaultMediaInfoDisplay(playerUiState)
|
|
||||||
},
|
|
||||||
buttons = { state ->
|
|
||||||
SetVolumeButton(
|
|
||||||
volumeUiState = volumeUiState,
|
|
||||||
onVolumeClick = onVolumeClick,
|
|
||||||
enabled = state.connected && state.media != null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
controlButtons = { playerUiController, playerUiState ->
|
|
||||||
PlayerScreenPodcastControlButtons(playerUiController, playerUiState)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ReportDrawnAfter {
|
|
||||||
playerViewModel.playerState.filterNotNull().first()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalHorologistApi::class)
|
|
||||||
@Composable
|
|
||||||
fun PlayerScreenPodcastControlButtons(
|
|
||||||
playerUiController: PlayerUiController,
|
|
||||||
playerUiState: PlayerUiState,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
PodcastControlButtons(
|
|
||||||
modifier = modifier,
|
|
||||||
playerController = playerUiController,
|
|
||||||
playerUiState = playerUiState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package com.birdsounds.identify.presentation
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
|
||||||
import androidx.media3.common.Player
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
|
||||||
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
|
||||||
import com.google.android.horologist.media.data.repository.PlayerRepositoryImpl
|
|
||||||
import com.google.android.horologist.media.model.Media
|
|
||||||
import com.google.android.horologist.media.ui.state.PlayerViewModel
|
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
@OptIn(ExperimentalHorologistApi::class)
|
|
||||||
class SpeakerPlayerViewModel(
|
|
||||||
playerRepository: PlayerRepositoryImpl,
|
|
||||||
player: Player,
|
|
||||||
audioFile: File,
|
|
||||||
audioFileUri: String
|
|
||||||
) : PlayerViewModel(playerRepository) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
playerRepository.connect(player) {}
|
|
||||||
if (audioFile.exists()) {
|
|
||||||
val media = Media(
|
|
||||||
id = "",
|
|
||||||
uri = audioFileUri,
|
|
||||||
title = "Recorded audio",
|
|
||||||
artist = ""
|
|
||||||
)
|
|
||||||
playerRepository.setMedia(media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val playerState = playerRepository.player
|
|
||||||
|
|
||||||
@ExperimentalHorologistApi
|
|
||||||
public companion object {
|
|
||||||
private const val TAG = "SpeakerPlayerViewModel"
|
|
||||||
|
|
||||||
public val Factory: ViewModelProvider.Factory = viewModelFactory {
|
|
||||||
initializer {
|
|
||||||
val application = this[APPLICATION_KEY]!!
|
|
||||||
|
|
||||||
val outputFileName = "audiorecord.opus"
|
|
||||||
val audioFile = File(application.filesDir, outputFileName)
|
|
||||||
val audioFileUri = application.filesDir.path + "/" + outputFileName
|
|
||||||
|
|
||||||
val player = ExoPlayer.Builder(application)
|
|
||||||
.setSeekForwardIncrementMs(5000L)
|
|
||||||
.setSeekBackIncrementMs(5000L)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
SpeakerPlayerViewModel(
|
|
||||||
PlayerRepositoryImpl(),
|
|
||||||
player,
|
|
||||||
audioFile,
|
|
||||||
audioFileUri
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package com.birdsounds.identify.presentation
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
|
||||||
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
|
|
||||||
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
|
|
||||||
import com.google.android.horologist.compose.layout.ScreenScaffold
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The composable responsible for displaying the main UI.
|
|
||||||
*
|
|
||||||
* This composable is stateless, and simply displays the state given to it.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun SpeakerRecordingScreen(
|
|
||||||
playbackState: PlaybackState,
|
|
||||||
isPermissionDenied: Boolean,
|
|
||||||
recordingProgress: Float,
|
|
||||||
onMicClicked: () -> Unit
|
|
||||||
) {
|
|
||||||
ScreenScaffold {
|
|
||||||
// Determine the control dashboard state.
|
|
||||||
// This converts the main app state into a control dashboard state for rendering
|
|
||||||
val controlDashboardUiState = computeControlDashboardUiState(
|
|
||||||
playbackState = playbackState,
|
|
||||||
isPermissionDenied = isPermissionDenied
|
|
||||||
)
|
|
||||||
ControlDashboard(
|
|
||||||
controlDashboardUiState = controlDashboardUiState,
|
|
||||||
onMicClicked = onMicClicked,
|
|
||||||
recordingProgress = recordingProgress
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeControlDashboardUiState(
|
|
||||||
playbackState: PlaybackState,
|
|
||||||
isPermissionDenied: Boolean
|
|
||||||
): ControlDashboardUiState =
|
|
||||||
when (playbackState) {
|
|
||||||
PlaybackState.PlayingVoice -> ControlDashboardUiState(
|
|
||||||
micState = ControlDashboardButtonUiState(
|
|
||||||
expanded = false,
|
|
||||||
enabled = false,
|
|
||||||
visible = false
|
|
||||||
),
|
|
||||||
playState = ControlDashboardButtonUiState(
|
|
||||||
expanded = true,
|
|
||||||
enabled = true,
|
|
||||||
visible = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
PlaybackState.Ready -> ControlDashboardUiState(
|
|
||||||
micState = ControlDashboardButtonUiState(
|
|
||||||
expanded = false,
|
|
||||||
enabled = !isPermissionDenied,
|
|
||||||
visible = true
|
|
||||||
),
|
|
||||||
playState = ControlDashboardButtonUiState(
|
|
||||||
expanded = false,
|
|
||||||
enabled = true,
|
|
||||||
visible = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
PlaybackState.Recording -> ControlDashboardUiState(
|
|
||||||
micState = ControlDashboardButtonUiState(
|
|
||||||
expanded = true,
|
|
||||||
enabled = true,
|
|
||||||
visible = true
|
|
||||||
),
|
|
||||||
playState = ControlDashboardButtonUiState(
|
|
||||||
expanded = false,
|
|
||||||
enabled = false,
|
|
||||||
visible = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PlaybackStatePreviewProvider : CollectionPreviewParameterProvider<PlaybackState>(
|
|
||||||
listOf(
|
|
||||||
PlaybackState.Ready,
|
|
||||||
PlaybackState.Recording,
|
|
||||||
PlaybackState.PlayingVoice
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@WearPreviewDevices
|
|
||||||
@WearPreviewFontScales
|
|
||||||
@Composable
|
|
||||||
fun SpeakerScreenPreview(
|
|
||||||
@PreviewParameter(PlaybackStatePreviewProvider::class) playbackState: PlaybackState
|
|
||||||
) {
|
|
||||||
SpeakerRecordingScreen(
|
|
||||||
playbackState = playbackState,
|
|
||||||
isPermissionDenied = true,
|
|
||||||
recordingProgress = 0.25f,
|
|
||||||
onMicClicked = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.birdsounds.identify.presentation
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
|
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
|
||||||
|
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
|
||||||
|
import com.google.android.horologist.compose.layout.ScreenScaffold
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StartRecordingScreen(
|
||||||
|
appState: AppState,
|
||||||
|
isPermissionDenied: Boolean,
|
||||||
|
onMicClicked: () -> Unit
|
||||||
|
) {
|
||||||
|
ScreenScaffold {
|
||||||
|
val controlDashboardUiState = computeControlDashboardUiState(
|
||||||
|
appState = appState,
|
||||||
|
isPermissionDenied = isPermissionDenied
|
||||||
|
)
|
||||||
|
ControlDashboard(
|
||||||
|
controlDashboardUiState = controlDashboardUiState,
|
||||||
|
onMicClicked = onMicClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeControlDashboardUiState(
|
||||||
|
appState: AppState,
|
||||||
|
isPermissionDenied: Boolean
|
||||||
|
): ControlDashboardUiState =
|
||||||
|
when (appState) {
|
||||||
|
AppState.Ready -> ControlDashboardUiState(
|
||||||
|
micState = ControlDashboardButtonUiState(
|
||||||
|
expanded = false,
|
||||||
|
enabled = !isPermissionDenied,
|
||||||
|
visible = true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
AppState.Recording -> ControlDashboardUiState(
|
||||||
|
micState = ControlDashboardButtonUiState(
|
||||||
|
expanded = true,
|
||||||
|
enabled = true,
|
||||||
|
visible = true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PlaybackStatePreviewProvider : CollectionPreviewParameterProvider<AppState>(
|
||||||
|
listOf(
|
||||||
|
AppState.Recording,
|
||||||
|
AppState.Ready
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@WearPreviewDevices
|
||||||
|
@WearPreviewFontScales
|
||||||
|
@Composable
|
||||||
|
fun SpeakerScreenPreview(
|
||||||
|
@PreviewParameter(PlaybackStatePreviewProvider::class) appState: AppState
|
||||||
|
) {
|
||||||
|
StartRecordingScreen(
|
||||||
|
appState = appState,
|
||||||
|
isPermissionDenied = true,
|
||||||
|
onMicClicked = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user