This commit is contained in:
2025-04-02 13:20:29 -04:00
parent 7ff235fb3f
commit e73d52d221
26 changed files with 1511 additions and 295 deletions

View File

@@ -44,21 +44,20 @@ dependencies {
include("*.jar")
})
api(files("libs/opus.aar"))
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
implementation("androidx.compose.ui:ui-tooling:1.3.1")
implementation("androidx.navigation:navigation-compose:2.8.0-rc01")
implementation("androidx.wear.compose:compose-navigation:1.3.1")
implementation("com.google.android.horologist:horologist-audio-ui:0.6.18")
implementation("com.google.android.horologist:horologist-audio:0.6.18")
implementation("com.google.android.horologist:horologist-compose-tools:0.6.18")
implementation("com.google.android.horologist:horologist-compose-tools:0.6.18")
implementation("androidx.compose.material:material-icons-core:1.6.8")
implementation("androidx.compose.material:material-icons-extended:1.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-data:0.6.8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1")
implementation("androidx.media3:media3-exoplayer:1.4.0")
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.ui.tooling)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.navigation.v131)
implementation(libs.horologist.audio.ui)
implementation(libs.horologist.audio)
implementation(libs.horologist.compose.tools)
implementation(libs.androidx.material.icons.core)
implementation(libs.androidx.material.icons.extended)
implementation(libs.horologist.compose.material)
implementation(libs.horologist.media.ui)
implementation(libs.horologist.media.data)
implementation(libs.kotlinx.coroutines.play.services)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.play.services.wearable)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
@@ -66,15 +65,20 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.foundation)
// implementation("androidx.compose.foundation:foundation:1.8.0-rc01")
implementation(libs.androidx.wear.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.compose.navigation)
implementation(libs.androidx.media3.common)
implementation("co.touchlab:stately-concurrent-collections:2.0.0")
implementation(libs.androidx.wear.ongoing)
implementation(libs.accompanist.flowlayout)
implementation(libs.stately.concurrent.collections)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.runtime.android) // androidTestImplementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.runtime.android)
implementation(libs.androidx.animation.core.android)
// androidTestImplementation(platform(libs.androidx.compose.bom))
// androidTestImplementation(libs.androidx.ui.test.junit4)
// debugImplementation(libs.androidx.ui.tooling)
// debugImplementation(libs.androidx.ui.test.manifest)

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.type.watch" />
@@ -26,6 +28,8 @@
<activity
android:name=".presentation.MainActivity"
android:exported="true"
android:alwaysRetainTaskState="true"
android:immersive="true"
android:taskAffinity=""
android:theme="@style/MainActivityTheme.Starting">
<intent-filter>

View File

@@ -0,0 +1,133 @@
package com.birdsounds.identify.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.sp
import androidx.wear.compose.material.Text
import kotlinx.coroutines.delay
@Composable
fun FlashingText(
text: String,
color: Color,
textDecor: TextDecoration = TextDecoration.None,
modifier: Modifier = Modifier,
key: Long,
fontStyle: FontStyle = FontStyle.Normal
) {
// Set up the flashing state
var isFlashing by remember { mutableStateOf(false) }
// Create animation for the background color
val backgroundColor by animateColorAsState(
targetValue = if (isFlashing) Color.DarkGray else Color.Transparent,
animationSpec = tween(durationMillis = 1000)
)
// Trigger the flash effect once
LaunchedEffect(key1 = key) {
isFlashing = true
delay(1500)
isFlashing = false
}
// Display the text with animated background
AutoSizedText(
text = text,
color = color,
textAlign = TextAlign.Center,
modifier = modifier
.background(color = backgroundColor),
textDecor = textDecor,
fontStyle = fontStyle
)
}
@Composable
fun AutoSizedText(
text: String,
color: Color = Color.White,
textAlign: TextAlign = TextAlign.Center,
modifier: Modifier = Modifier,
textDecor: TextDecoration = TextDecoration.None,
minFontSize: TextUnit = 5.sp,
targetFontSize: TextUnit = 24.sp,
fontWeight: FontWeight = FontWeight.Normal,
fontStyle: FontStyle = FontStyle.Normal,
) {
var display_text = text;
val textMeasurer = rememberTextMeasurer()
BoxWithConstraints(modifier) {
val density = LocalDensity.current
// val minFontSizePx = with(density) { minFontSize.toPx() }
// val maxFontSizePx = with(density) { targetFontSize.toPx() }
val availableWidthPx = with(density) { maxWidth.toPx() }
// Binary search to find the appropriate font size that fits the available width
var lowPx: TextUnit = minFontSize
var highPx: TextUnit = targetFontSize
var bestFontSizePx = targetFontSize;
val textStyle = TextStyle(fontWeight = fontWeight, color = color, fontSize=bestFontSizePx)
while (lowPx <= highPx) {
val midPx: TextUnit = TextUnit(value=lowPx.value/2 + highPx.value/2 , type= TextUnitType.Sp)
val style = textStyle.copy(fontSize = midPx)
val textLayoutResult = textMeasurer.measure(
text = display_text,
style = style,
maxLines = 1,
softWrap = false,
density = density,
)
if (textLayoutResult.size.width <= availableWidthPx) {
// // This font size fits, try a larger oneb
bestFontSizePx = midPx;
break;
} else {
// // This font size is too large, try a smaller one
// highPx = (midPx - 1).toFloat()
highPx = TextUnit(value=midPx.value - 1, type=TextUnitType.Sp)
}
}
// Convert the best font size back to sp and use it
// val bestFontSize = with(density) { bestFontSizePx.toSp() }
Text(
text = display_text,
color = color,
fontSize = bestFontSizePx,
fontStyle = fontStyle,
fontWeight = fontWeight,
maxLines = 1,
textDecoration = textDecor,
overflow = TextOverflow.Clip,
modifier = Modifier.fillMaxWidth(),
textAlign = textAlign
)
}
}

View File

@@ -1,68 +0,0 @@
package com.birdsounds.identify.presentation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.wear.compose.material.CompactChip
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
@Composable
fun ControlDashboard(controlDashboardUiState: ControlDashboardUiState,
onMicClicked: () -> Unit,
onNavClicked: () -> Unit,
modifier: Modifier = Modifier) {
Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ControlDashboardButton(buttonState = controlDashboardUiState.micState,
onClick = onMicClicked,
labelText = if (controlDashboardUiState.micState.expanded) {
"Stop"
} else {
"Start"
})
}
}
}
@Composable
private fun ControlDashboardButton(buttonState: ControlDashboardButtonUiState,
onClick: () -> Unit,
labelText: String,
modifier: Modifier = Modifier) {
CompactChip(modifier = Modifier,
onClick = onClick,
enabled = true,
contentPadding = PaddingValues(5.dp, 1.dp, 5.dp, 1.dp),
shape = MaterialTheme.shapes.small,
label = {
Text(text = labelText)
})
}
// Button(modifier = modifier, enabled = buttonState.enabled && buttonState.visible, onClick = onClick) {
// Text(contentDescription);
//// Icon(imageVector = imageVector, contentDescription = contentDescription)
// }
//}
data class ControlDashboardButtonUiState(val expanded: Boolean, val enabled: Boolean, val visible: Boolean)
data class ControlDashboardUiState(val micState: ControlDashboardButtonUiState) {
init { // Check that at most one of the buttons is expanded
}
}

View File

@@ -6,56 +6,62 @@
package com.birdsounds.identify.presentation
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat.startForeground
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.wear.ambient.AmbientLifecycleObserver
import androidx.wear.compose.material.CompactChip
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
import androidx.wear.ongoing.OngoingActivity
import com.birdsounds.identify.R
import com.birdsounds.identify.presentation.theme.IdentifyTheme
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.audio.ui.VolumeViewModel
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
import com.google.android.horologist.compose.material.Chip
import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding
import com.google.android.horologist.compose.material.ResponsiveListHeader
import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
val flow_stream = MutableStateFlow<String>("")
val Any.TAG: String
@@ -64,26 +70,30 @@ val Any.TAG: String
return if (tag.length <= 23) tag else tag.substring(0, 23)
}
class MainActivity : ComponentActivity() {
lateinit var ambientObserver:AmbientLifecycleObserver
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
ambientObserver= AmbientLifecycleObserver(this.findActivity(), ambientCallback)
super.onCreate(savedInstanceState)
lifecycle.addObserver(ambientObserver)
setTheme(android.R.style.Theme_DeviceDefault)
setContent {
WearApp()
}
}
}
@SuppressLint("ForegroundServiceType")
@OptIn(ExperimentalHorologistApi::class)
@Composable
@Preview
fun WearApp() {
IdentifyTheme {
lateinit var requestPermissionLauncher: ManagedActivityResultLauncher<String, Boolean>
val context = LocalContext.current
@@ -91,6 +101,48 @@ fun WearApp() {
val scope = rememberCoroutineScope()
val touchIntent =
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val notificationBuilder =
NotificationCompat.Builder(context, "Identify Watch App")
.setOngoing(true)
.setSmallIcon(com.birdsounds.identify.R.mipmap.ic_launcher)
val ongoingActivity =
OngoingActivity.Builder(
context, 4, notificationBuilder
).setTouchIntent(touchIntent)
.setStaticIcon(
com.birdsounds.identify.R.mipmap.ic_launcher)
.build()
ongoingActivity.apply(context)
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(
4,
notificationBuilder.build()
)
val notification: Notification = NotificationCompat.Builder(
context, "Identify Watch App"
)
.setContentTitle("Always On App")
.setContentText("Running in foreground")
.setSmallIcon(com.birdsounds.identify.R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
val volumeViewModel: VolumeViewModel = viewModel(factory = VolumeViewModel.Factory)
val mainState = remember(activity) {
@@ -99,7 +151,6 @@ fun WearApp() {
})
}
requestPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) {
scope.launch {
mainState.permissionResultReturned()
@@ -108,42 +159,111 @@ fun WearApp() {
val navController: NavHostController = rememberSwipeDismissableNavController()
mainState.setNavController(navController);
SwipeDismissableNavHost(navController = navController, userSwipeEnabled = true, startDestination = "speaker") {
SwipeDismissableNavHost(
navController = navController,
userSwipeEnabled = true,
startDestination = "speaker"
) {
composable("species_list") {
ScreenScaffold {
SpeciesListView(context = context)
SpeciesListView(context = context, appState = mainState.appState)
}
}
composable("speaker") {
ScreenScaffold {
CompactChip(modifier = Modifier,
onClick = { navController.navigate("species_list") },
enabled = true,
contentPadding = PaddingValues(5.dp, 1.dp, 5.dp, 1.dp),
shape = MaterialTheme.shapes.small,
label = {
Text(text = "View>")
})
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
) {
Row() {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
// Define the animation for the scale
val scale by animateFloatAsState(
targetValue = if (isPressed) 2.0f else 1.0f
)
CompactChip(
onClick = {
vibrate(
context,
250
);navController.navigate("species_list")
},
enabled = true,
modifier = Modifier.scale(scale),
interactionSource = interactionSource,
contentPadding = PaddingValues(5.dp, 1.dp, 5.dp, 1.dp),
shape = MaterialTheme.shapes.small,
label = {
Text(text = "Show List")
},
)
StartRecordingScreen(
appState = mainState.appState,
onMicClicked = {
scope.launch {
mainState.onMicClicked()
}
},
onNavClicked = {
navController.navigate("species_list")
}
Row() {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
)
// Define the animation for the scale
val scale by animateFloatAsState(
targetValue = if (isPressed) 2.0f else 1.0f
)
CompactChip(
onClick = {
vibrate(context, 250);
scope.launch {
mainState.onMicClicked()
}
},
modifier = Modifier.scale(scale),
interactionSource = interactionSource,
enabled = true,
contentPadding = PaddingValues(5.dp, 1.dp, 5.dp, 1.dp),
shape = MaterialTheme.shapes.small,
label = {
Text(text = if (mainState.appState == AppState.Recording) "Stop Recording" else "Start Recording")
})
}
Row() {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
// Define the animation for the scale
val scale by animateFloatAsState(
targetValue = if (isPressed) 2.0f else 1.0f
)
CompactChip(
onClick = {
vibrate(context, 250);
species_list_show.clear();
internal_list.clear();
},
enabled = true,
modifier = Modifier.scale(scale),
interactionSource = interactionSource,
contentPadding = PaddingValues(5.dp, 1.dp, 5.dp, 1.dp),
shape = MaterialTheme.shapes.small,
label = {
Text(text = "Clear List")
},
)
}
}
}
}
}
}

View File

@@ -31,6 +31,9 @@ class MainState(private val activity: Activity, private val requestPermission: (
public fun setNavController(_navController: NavHostController) {
navController = _navController
}
suspend fun onMicClicked() {
playbackStateMutatorMutex.mutate {
when (appState) {

View File

@@ -1,28 +1,58 @@
package com.birdsounds.identify.presentation
import android.content.Context
import androidx.compose.ui.platform.LocalContext
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService
import kotlinx.coroutines.delay
import java.nio.ByteBuffer
var last_message_recv_tstamp: Long = 0L;
class MessageListenerService : WearableListenerService() {
private val tag = "MessageListenerService"
lateinit var this_context: Context;
fun set_context(context: Context)
{
this_context = context;
}
override fun onMessageReceived(p0: MessageEvent) {
super.onMessageReceived(p0)
val t_scored = ByteBuffer.wrap(p0.data).getLong()
last_message_recv_tstamp = t_scored;
var byte_strings: ByteArray = p0.data.copyOfRange(8, p0.data.size)
var score_species_string = byte_strings.decodeToString()
var list_strings: List<String> = score_species_string.split(';')
var do_vibrate = false;
var did_add = false;
var new_entries = 0;
SpeciesList.clear_new_flags();
list_strings.map({
var split_str = it.split(',')
if (split_str.size == 2) {
var out = AScore(split_str[0], split_str[1].toFloat(), t_scored)
if (out.score > 0.005) {
SpeciesList.add_observation(out)
new_entries+=1;
var out = AScore(split_str[0], split_str[1].toFloat(), t_scored);
out.new_entry = true;
if (SpeciesList.add_observation(out))
{
did_add = true;
}
do_vibrate = true;
}
})
// SpeciesList.set_num_new_entries(new_entries);
// SpeciesList.insert_new_entry_spacer(new_entries);
if (did_add) {
vibrateDouble(this, 100, 250, 100);
} else if (do_vibrate) {
vibrate(this,100)
}
// if (new_entries > 0) {
SpeciesList.update_list_on_ui_thread();
// }
MessageSender.messageLog.add(t_scored)
}
}

View File

@@ -0,0 +1,19 @@
package com.birdsounds.identify.presentation
import androidx.wear.ambient.AmbientLifecycleObserver
val ambientCallback = object : AmbientLifecycleObserver.AmbientLifecycleCallback {
override fun onEnterAmbient(ambientDetails: AmbientLifecycleObserver.AmbientDetails) {
// ... Called when moving from interactive mode into ambient mode.
}
override fun onExitAmbient() {
// ... Called when leaving ambient mode, back into interactive mode.
}
override fun onUpdateAmbient() {
// ... Called by the system in order to allow the app to periodically
// update the display while in ambient mode. Typically the system will
// call this every 60 seconds.
}
}

View File

@@ -5,11 +5,13 @@ import com.theeasiestway.opus.Opus
import android.Manifest
import android.content.Context
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AutomaticGainControl
import android.media.audiofx.NoiseSuppressor
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresPermission
import encodePcmToAac
@@ -21,6 +23,66 @@ import java.time.Instant
/**
* A helper class to provide methods to record audio input from the MIC to the internal storage.
*/
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
fun vibrateDouble(
context: Context,
firstDuration: Long = 100,
pauseDuration: Long = 200,
secondDuration: Long = 100
) {
// Get the vibrator service
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
// Create a pattern for double vibration: 0ms delay, firstDuration vibrate, pauseDuration pause, secondDuration vibrate
val vibrationPattern = longArrayOf(0, firstDuration, pauseDuration, secondDuration)
// The -1 parameter means don't repeat the pattern
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createWaveform(vibrationPattern, -1)
vibrator.vibrate(vibrationEffect)
} else {
// Deprecated method for older API levels
@Suppress("DEPRECATION")
vibrator.vibrate(vibrationPattern, -1)
}
}
fun vibrate(context: Context, duration: Long = 500) {
val vibrator = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createOneShot(
duration, // duration in milliseconds
VibrationEffect.DEFAULT_AMPLITUDE // strength of vibration
)
vibrator.vibrate(vibrationEffect)
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration) // Vibration for 500ms
}
}
@Suppress("DEPRECATION")
class SoundRecorder(
context_in: Context,
@@ -44,7 +106,7 @@ class SoundRecorder(
var noiseSuppressor: NoiseSuppressor? = null
var automaticGainControl: AutomaticGainControl? = null
var chunk_index: Int = 0
val audioSource = MediaRecorder.AudioSource.DEFAULT
val audioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION
val sampleRateInHz = 48000
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT

View File

@@ -2,8 +2,11 @@ package com.birdsounds.identify.presentation
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import java.time.Instant
@@ -12,16 +15,24 @@ class AScore(
_species: String,
_score: Float,
_timestamp: Long,
_trigger: Boolean = false
) {
_trigger: Boolean = false,
_new_entry: Boolean = false,
_redraw_number: Long = 0,
) {
var redraw_number = _redraw_number;
val split_stuff: List<String> = _species.split("_");
val species = split_stuff[0];
val score = _score;
var trigger = _trigger;
var new_entry = _new_entry;
// var common_name = split_stuff[1];
val common_name = if (split_stuff.size > 1) split_stuff[1] else "";
val timestamp = _timestamp
fun set_redraw_number(redraw_num: Long)
{
redraw_number = redraw_num
}
fun age(): Long {
var tstamp: Long = Instant.now().toEpochMilli()
return (tstamp - timestamp)
@@ -34,20 +45,28 @@ class AScore(
}
}
var internal_list = mutableListOf<AScore>()
object SpeciesList {
var internal_list = mutableListOf<AScore>()
var trigger_redraw = 0;
var num_new_entries = 0;
var do_add_observation = false
var _list_on_ui: SnapshotStateList<MutableState<AScore>>? = null
fun setSpeciecsShow_list(list_in: SnapshotStateList<MutableState<AScore>>) {
_list_on_ui = list_in;
}
fun clear_new_flags() {
for (i in internal_list) {
i.new_entry = false;
}
}
fun add_observation(species_in: AScore) {
fun add_observation(species_in: AScore): Boolean {
Log.w(TAG, "In add observation")
var idx = 0
var idx_replace = -1
var added_new = false;
for (i in internal_list) {
if (i.species == species_in.species) {
idx_replace = idx
@@ -60,22 +79,31 @@ object SpeciesList {
internal_list[idx_replace].trigger = true;
} else {
internal_list.add(species_in)
added_new = true;
}
// internal_list = internal_list.sortedBy({ (it.age()) }).toMutableList()
internal_list = internal_list.sortedWith(compareBy({ it.age() }, { it.score})).toMutableList()
internal_list =
internal_list.sortedWith(compareBy({ it.age() }, { it.score })).toMutableList()
return added_new
}
fun update_list_on_ui_thread(
) {
while (_list_on_ui!!.size < internal_list.size) {
_list_on_ui!!.add(mutableStateOf(AScore("",0.0F,0L)))
_list_on_ui!!.add(mutableStateOf(AScore("", 0.0F, 0L)))
}
val time_stamp = System.currentTimeMillis()
for ((index, value) in internal_list.withIndex()) {
_list_on_ui?.get(index)?.value = value;
}
Log.w(TAG, internal_list.size.toString())
Log.w(TAG, _list_on_ui?.size.toString()) // internal_list.add(species_in)
}
}

View File

@@ -2,23 +2,22 @@ package com.birdsounds.identify.presentation
import android.annotation.SuppressLint
import android.content.Context
import android.text.format.DateFormat
import android.util.Log
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Mic
import androidx.compose.material.icons.rounded.MicOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -27,124 +26,228 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.lerp
import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults.scalingParams
import androidx.wear.compose.foundation.lazy.ScalingParams
import androidx.wear.compose.foundation.curvedComposable
import androidx.wear.compose.foundation.lazy.items
import androidx.wear.compose.material.Text
import androidx.wear.compose.foundation.lazy.itemsIndexed
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.TimeTextDefaults
import androidx.wear.compose.material.TimeText
import androidx.wear.compose.material.curvedText
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
import kotlinx.coroutines.delay
import java.time.Instant
import java.util.Locale
fun interpolateColor(value: Float): Color {
// Ensure that the input value is clamped between 0 and 1
val clampedValue = value.coerceIn(0f, 1f)
// Ensure that the input value is clamped between 0 and 1
val clampedValue = value.coerceIn(0f, 1f)
// Red component: starts at 255 (white) and decreases to 0
val red = (255 * (1 - clampedValue)).toInt()
// Red component: starts at 255 (white) and decreases to 0
val red = (255 * (1 - clampedValue)).toInt()
// Green component: stays at 255 (since both white and green have full green)
val green = 255
// Green component: stays at 255 (since both white and green have full green)
val green = 255
// Blue component: starts at 255 (white) and decreases to 0
val blue = (255 * (1 - clampedValue)).toInt()
// Blue component: starts at 255 (white) and decreases to 0
val blue = (255 * (1 - clampedValue)).toInt()
// Construct the color
return Color(red.toInt(), green.toInt(), blue.toInt())
}
@Composable
fun FlashingText(
text: String,
color: Color,
modifier: Modifier = Modifier
) {
// Set up the flashing state
var isFlashing by remember { mutableStateOf(false) }
// Create animation for the background color
val backgroundColor by animateColorAsState(
targetValue = if (isFlashing) Color.Cyan else Color.Transparent,
animationSpec = tween(durationMillis = 500)
)
// Trigger the flash effect once
LaunchedEffect(key1 = true) {
isFlashing = true
delay(300)
isFlashing = false
}
// Display the text with animated background
Text(
text = text,
color = color,
modifier = modifier
.background(color = backgroundColor)
)
// Construct the color
return Color(red.toInt(), green.toInt(), blue.toInt())
}
val species_list_show = mutableStateListOf<MutableState<AScore>>()
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun SpeciesListView(context: Context,
fun SpeciesListView(
context: Context, appState: AppState,
) {
val species_list_show = mutableStateListOf<MutableState<AScore>>()
for (i in 1..3)
{
val hi = mutableStateOf(AScore("",0.0F,0L))
for (i in 1..3) {
val hi = mutableStateOf(AScore("", 0.0F, 0L))
species_list_show.add(hi);
}
SpeciesList.setSpeciecsShow_list(species_list_show)
val species_show: SnapshotStateList<MutableState<AScore>> = remember { species_list_show }
// var sP = scalingParams( maxTransitionArea= 0.25f, minTransitionArea = 0.05f)
// val columnState = ScalingLazyColumnState(scalingParams = sP);
var columnState = rememberResponsiveColumnState(contentPadding = ScalingLazyColumnDefaults.padding(first = ScalingLazyColumnDefaults.ItemType.Text,
last = ScalingLazyColumnDefaults.ItemType.Chip), verticalArrangement= Arrangement.spacedBy(
space = 1.dp,
alignment = Alignment.Top,
),)
var columnState = rememberResponsiveColumnState(
contentPadding = ScalingLazyColumnDefaults.padding(
first = ScalingLazyColumnDefaults.ItemType.Text,
last = ScalingLazyColumnDefaults.ItemType.Chip
),
verticalArrangement = Arrangement.spacedBy(
space = 1.dp,
alignment = Alignment.Top,
),
)
val text_counter = remember { mutableStateOf("Initial text") }
LaunchedEffect(key1 = true) {
while (true) {
// Update the text - you can put your custom logic here
// For example, showing current time:
var lag = ((Instant.now().toEpochMilli() - last_message_recv_tstamp) / 1000F).toInt();
if (last_message_recv_tstamp == 0L) {
text_counter.value = "No msg recv"
} else {
text_counter.value = "${lag}s"
}
ScreenScaffold(scrollState = columnState) {
ScalingLazyColumn(
columnState = columnState,
modifier = Modifier.fillMaxWidth()
) {
items(species_show) { aSpec ->
if (aSpec.value.trigger)
{
Log.w(TAG,"Trigger "+aSpec.toString())
FlashingText(text = aSpec.value.common_name, color = interpolateColor(aSpec.value.score))
aSpec.value.trigger = false;
} else
{
Text(text = aSpec.value.common_name, color = interpolateColor(aSpec.value.score))
}
}
} // Dynamically display the chips
// Delay for 1 second
delay(1000)
}
}
ScreenScaffold(
scrollState = columnState
) {
TimeText(
timeSource =
TimeTextDefaults.timeSource(
DateFormat.getBestDateTimePattern(Locale.getDefault(), "hh:mm")
),
startCurvedContent = {
curvedComposable {
Icon(
if (appState == AppState.Recording) Icons.Rounded.Mic else Icons.Rounded.MicOff,
contentDescription = "",
modifier = Modifier.size(16.dp)
// tint = androidx.compose.ui.graphics.Color.Red
)
}
// curvedText(
// text = if (appState == AppState.Recording) "" else "Inactive0",
//// textAlign = TextAlign.Center,
// )
},
endCurvedContent = {
curvedText(text = text_counter.value)//, textAlign = TextAlign.Center)
}
)
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
) {
ScalingLazyColumn(
columnState = columnState,
modifier = Modifier.fillMaxWidth()
) {
itemsIndexed(
species_show,
) { index, aSpec ->
var text_decor = TextDecoration.None
var text_style = FontStyle.Normal;
val currentName = aSpec.value.common_name
val currentScore = aSpec.value.score
val isNewEntry = aSpec.value.new_entry
val shouldTrigger = aSpec.value.trigger
var add_sep = false;
if (aSpec.value.new_entry) {
// add_sep = true;
text_style = FontStyle.Italic;
// aSpec.value.new_entry = false
}
Log.w(TAG, "Species " + currentName + " " + aSpec.value.new_entry.toString())
LaunchedEffect(Unit) {
delay(100);
aSpec.value.new_entry = false
}
// if (index == (SpeciesList.num_new_entries-1)) {
// add_sep = true;"
// }
Log.w(
TAG,
"Adding Separator: " + index.toString() + " " + SpeciesList.num_new_entries + " " + add_sep.toString()
)
aSpec.value.new_entry = false;
// var composables: List<@Composable () -> Unit> = listOf();
key(currentName, isNewEntry, shouldTrigger) {
if (shouldTrigger) {
Log.w(TAG, "Trigger " + aSpec.toString())
FlashingText(
text = aSpec.value.common_name,
fontStyle = text_style,
color = interpolateColor(aSpec.value.score),
key = Instant.now().toEpochMilli(),
textDecor = text_decor,
modifier = Modifier.fillMaxSize()
)
LaunchedEffect(Unit) {
delay(100);
aSpec.value.trigger = false
}
Log.e("TAG", "BLINKING")
} else {
AutoSizedText(
text = aSpec.value.common_name,
color = interpolateColor(aSpec.value.score),
modifier = Modifier.fillMaxSize(),
textAlign = TextAlign.Center,
textDecor = text_decor,
fontStyle = text_style,
)
Log.e("TAG", "Not blinking")
}
}
if (add_sep) {
// composables += {
AutoSizedText(
text = "-------------------",
modifier = Modifier.fillMaxSize(),
textAlign = TextAlign.Center,
)
// }
}
// Column {
// composables.forEach { composable ->
// Row {
// composable()
// }
// }
// }
}// Dynamically display the chips
}
}
}
}
// ScreenScaffold(scrollState = columnState) {
// ScalingLazyColumn(columnState = columnState, modifier = Modifier.fillMaxSize()) {
// items(species_show) { aSpec -> Text(text = aSpec.value.common_name)

View File

@@ -1,45 +0,0 @@
package com.birdsounds.identify.presentation
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.navigation.NavHostController
import com.google.android.horologist.compose.layout.ScreenScaffold
@Composable
fun StartRecordingScreen(
appState: AppState,
onMicClicked: () -> Unit,
onNavClicked: () -> Unit
) {
ScreenScaffold {
val controlDashboardUiState = computeControlDashboardUiState(
appState = appState,
)
ControlDashboard(
controlDashboardUiState = controlDashboardUiState,
onMicClicked = onMicClicked,
onNavClicked = onNavClicked
)
}
}
private fun computeControlDashboardUiState(
appState: AppState,
): ControlDashboardUiState =
when (appState) {
AppState.Ready -> ControlDashboardUiState(
micState = ControlDashboardButtonUiState(expanded = false, visible = true, enabled = true),
)
AppState.Recording -> ControlDashboardUiState(
micState = ControlDashboardButtonUiState(expanded = true, visible = true, enabled = true),
)
}
private class PlaybackStatePreviewProvider : CollectionPreviewParameterProvider<AppState>(
listOf(
AppState.Recording,
AppState.Ready
)
)

View File

@@ -2,7 +2,7 @@
<style name="MainActivityTheme.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@android:color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@android:style/Theme.DeviceDefault</item>
</style>
</resources>