diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index dcda84b..9818a9c 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,18 +4,29 @@
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index d4b7acc..c22b6fa 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/workspace (SFConflict ispatel 2025-03-10-10-03-26).xml b/.idea/workspace (SFConflict ispatel 2025-03-10-10-03-26).xml
new file mode 100644
index 0000000..34de4d8
--- /dev/null
+++ b/.idea/workspace (SFConflict ispatel 2025-03-10-10-03-26).xml
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+
+
+
+ @android:style/Theme.DeviceDefault
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "associatedIndex": 0
+}
+
+
+
+
+
+ {
+ "keyToString": {
+ "Android App.mobile.executor": "Run",
+ "Android App.wear.executor": "Run",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.cidr.known.project.marker": "true",
+ "RunOnceActivity.readMode.enableVisualFormatting": "true",
+ "cf.first.check.clang-format": "false",
+ "cidr.known.project.marker": "true",
+ "com.google.services.firebase.aqiPopupShown": "true",
+ "git-widget-placeholder": "master",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/isp/Seafile/Designs/Android/bird_sound_identify_wearos",
+ "project.structure.last.edited": "Modules",
+ "project.structure.proportion": "0.17",
+ "project.structure.side.proportion": "0.2"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1741017174616
+
+
+ 1741017174616
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace (SFConflict ispatel@live.com 2025-03-08-01-11-41).xml b/.idea/workspace (SFConflict ispatel@live.com 2025-03-08-01-11-41).xml
new file mode 100644
index 0000000..34de4d8
--- /dev/null
+++ b/.idea/workspace (SFConflict ispatel@live.com 2025-03-08-01-11-41).xml
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+
+
+
+ @android:style/Theme.DeviceDefault
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "associatedIndex": 0
+}
+
+
+
+
+
+ {
+ "keyToString": {
+ "Android App.mobile.executor": "Run",
+ "Android App.wear.executor": "Run",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.cidr.known.project.marker": "true",
+ "RunOnceActivity.readMode.enableVisualFormatting": "true",
+ "cf.first.check.clang-format": "false",
+ "cidr.known.project.marker": "true",
+ "com.google.services.firebase.aqiPopupShown": "true",
+ "git-widget-placeholder": "master",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/isp/Seafile/Designs/Android/bird_sound_identify_wearos",
+ "project.structure.last.edited": "Modules",
+ "project.structure.proportion": "0.17",
+ "project.structure.side.proportion": "0.2"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1741017174616
+
+
+ 1741017174616
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6b25f53..0162e2d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,11 +1,23 @@
[versions]
+accompanistFlowlayout = "0.28.0"
agp = "8.8.1"
+composeNavigationVersion = "1.3.1"
+horologistComposeTools = "0.6.18"
+horologistAudio = "0.6.18"
+horologistMediaData = "0.6.8"
+horologistMediaUi = "0.6.8"
kotlin = "2.0.0"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
+kotlinxCoroutinesPlayServices = "1.8.1"
+lifecycleViewmodelCompose = "2.8.4"
+materialIconsCore = "1.6.8"
+materialIconsExtended = "1.7.8"
+media3Exoplayer = "1.4.0"
+navigationCompose = "2.8.0-rc01"
playServicesWearable = "18.2.0"
material = "1.10.0"
activity = "1.9.1"
@@ -13,6 +25,9 @@ constraintlayout = "2.1.4"
composeBom = "2024.04.01"
composeMaterial = "1.2.1"
composeFoundation = "1.2.1"
+statelyConcurrentCollections = "2.0.0"
+uiTooling = "1.3.1"
+wearOngoing = "1.0.0"
wearToolingPreview = "1.0.0"
activityCompose = "1.9.1"
coreSplashscreen = "1.0.1"
@@ -22,14 +37,32 @@ composeMaterial3 = "1.0.0-alpha23"
workRuntimeKtx = "2.9.1"
lifecycleRuntimeKtx = "2.6.1"
runtimeAndroid = "1.6.6"
+datastoreCoreAndroid = "1.1.3"
+coreKtxVersion = "1.13.0"
+animationCoreAndroid = "1.6.6"
#litert = "1.0.1"
[libraries]
+accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanistFlowlayout" }
+androidx-compose-navigation-v131 = { module = "androidx.wear.compose:compose-navigation", version.ref = "composeNavigationVersion" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
+androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCore" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
+androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wearOngoing" }
+horologist-audio-ui = { module = "com.google.android.horologist:horologist-audio-ui", version.ref = "horologistComposeTools" }
+horologist-audio = { module = "com.google.android.horologist:horologist-audio", version.ref = "horologistAudio" }
+horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologistMediaData" }
+horologist-compose-tools = { module = "com.google.android.horologist:horologist-compose-tools", version.ref = "horologistComposeTools" }
+horologist-media-ui = { module = "com.google.android.horologist:horologist-media-ui", version.ref = "horologistMediaUi" }
+horologist-media-data = { module = "com.google.android.horologist:horologist-media-data", version.ref = "horologistMediaData" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" }
play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
@@ -53,10 +86,15 @@ androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx"
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" }
+androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastoreCoreAndroid" }
+core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtxVersion" }
+stately-concurrent-collections = { module = "co.touchlab:stately-concurrent-collections", version.ref = "statelyConcurrentCollections" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" }
+androidx-animation-core-android = { group = "androidx.compose.animation", name = "animation-core-android", version.ref = "animationCoreAndroid" }
#litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "litert" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.0.20" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.1.10" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts
index bf5e01d..d8049b4 100644
--- a/mobile/build.gradle.kts
+++ b/mobile/build.gradle.kts
@@ -55,12 +55,16 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
+
+ implementation("androidx.datastore:datastore-preferences:1.1.3")
implementation("org.tensorflow:tensorflow-lite:2.6.0")
// implementation("com.google.android.gms:play-services-tflite:20.0.0")
implementation("uk.me.berndporr:iirj:1.7")
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
+ implementation(libs.androidx.datastore.core.android)
+ implementation(libs.core.ktx)
// implementation(libs.litert)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/mobile/src/main/java/com/birdsounds/identify/phone_Downloader.kt b/mobile/src/main/java/com/birdsounds/identify/phone_Downloader.kt
index 1840e7f..e0ab221 100644
--- a/mobile/src/main/java/com/birdsounds/identify/phone_Downloader.kt
+++ b/mobile/src/main/java/com/birdsounds/identify/phone_Downloader.kt
@@ -6,7 +6,7 @@ import java.io.IOException
class Downloader(mainActivity: MainActivity) {
- private val settings = Settings();
+ private val settings = Settings;
private var activity: MainActivity = mainActivity;
private var context: Context = activity.applicationContext;
diff --git a/mobile/src/main/java/com/birdsounds/identify/phone_Location.kt b/mobile/src/main/java/com/birdsounds/identify/phone_Location.kt
index e1d44f6..cd63625 100644
--- a/mobile/src/main/java/com/birdsounds/identify/phone_Location.kt
+++ b/mobile/src/main/java/com/birdsounds/identify/phone_Location.kt
@@ -1,15 +1,24 @@
+@file:Suppress("DEPRECATION")
+
package com.birdsounds.identify
import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
+import android.location.Address
+import android.location.Geocoder
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.util.Log
+import android.widget.TextView
import android.widget.Toast
import androidx.core.app.ActivityCompat
+import java.util.Locale
+
object Location {
private var locationListenerGPS: LocationListener? = null
@@ -30,7 +39,10 @@ object Location {
) {
val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+
+
if (locationListenerGPS == null) locationListenerGPS = object : LocationListener {
+ @SuppressLint("SetTextI18n")
override fun onLocationChanged(location: Location) {
Log.w(TAG, "Got location changed");
while (!soundClassifier.is_model_ready()) {
@@ -39,7 +51,35 @@ object Location {
Log.w(TAG, "Sound classifier is ready");
soundClassifier.runMetaInterpreter(location)
- soundClassifier.runMetaInterpreter(location)
+ val activity = context as? Activity;
+ activity?.let {
+
+ val text_species: TextView = it.findViewById(R.id.local_species)
+ text_species.text = local_species;
+
+ val loc_lon: TextView = it.findViewById(R.id.location_long)
+ loc_lon.text =
+ "Longitude: ${location.longitude}"
+
+ val loc_lat: TextView = it.findViewById(R.id.location_lat)
+ loc_lat.text =
+ "Latitude: ${location.latitude}"
+
+ val loc_string: TextView = it.findViewById(R.id.location_string);
+ val geocoder = Geocoder(context, Locale.getDefault())
+ val addresses: MutableList
? =
+ geocoder.getFromLocation(location.latitude, location.longitude, 1)
+ if (addresses?.isNotEmpty() == true) {
+ val address: Address = addresses[0]
+
+// loc_string.text = address.locality.toString() + ", " + address.adminArea.toString() + " " + address.countryName.toString()
+ loc_string.text = address.getAddressLine(0).toString()
+// Log.w(TAG, address.toString())
+
+
+ }
+ }
+
}
@Deprecated("")
@@ -53,7 +93,7 @@ object Location {
}
}
locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER, 60000, 0f,
+ LocationManager.PASSIVE_PROVIDER, 60000, 0f,
locationListenerGPS!!
)
}
diff --git a/mobile/src/main/java/com/birdsounds/identify/phone_MainActivity.kt b/mobile/src/main/java/com/birdsounds/identify/phone_MainActivity.kt
index 8def748..b91c767 100644
--- a/mobile/src/main/java/com/birdsounds/identify/phone_MainActivity.kt
+++ b/mobile/src/main/java/com/birdsounds/identify/phone_MainActivity.kt
@@ -1,16 +1,42 @@
package com.birdsounds.identify
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
-import android.Manifest
+import android.preference.PreferenceManager
import android.util.Log
+import android.widget.SeekBar
+import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import androidx.lifecycle.lifecycleScope
import com.google.android.gms.wearable.ChannelClient
import com.google.android.gms.wearable.Wearable
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import java.time.Instant
+
+private var updateJob: Job? = null
+private var updateCounter = 0
+val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
+
+
val Any.TAG: String
get() {
@@ -19,11 +45,39 @@ val Any.TAG: String
}
+class SynchronousDataStore(private val context: Context) {
+
+ // Define keys
+ companion object {
+ val THRESHOLD_KEY = intPreferencesKey("user_age")
+ }
+
+ // Synchronous write operations
+ fun saveThreshold(value: Int) {
+ runBlocking {
+ context.dataStore.edit { preferences ->
+ preferences[THRESHOLD_KEY] = value;
+ }
+ }
+ }
+
+ fun getThreshold(): Int {
+
+ return runBlocking {
+ context.dataStore.data.first()[THRESHOLD_KEY] ?: 50
+ }
+ }
+}
+
+
+
class MainActivity : AppCompatActivity() {
// private lateinit var soundClassifier: SoundClassifier
val REQUEST_PERMISSIONS = 1337
+ private lateinit var dataStore: SynchronousDataStore
+ private lateinit var last_message_delay: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -49,6 +103,34 @@ class MainActivity : AppCompatActivity() {
insets
}
+
+ val thresholdText = findViewById(R.id.threshold_value_text)
+ val seekBar = findViewById(R.id.threshold_set_scale_bar)
+ last_message_delay = findViewById(R.id.last_message_delay)
+ dataStore = SynchronousDataStore(applicationContext)
+
+
+
+
+
+ Settings.threshold = dataStore.getThreshold();
+ seekBar.progress = Settings.threshold
+ thresholdText.text = String.format("%.2f", Settings.threshold/100f)
+ seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ @SuppressLint("DefaultLocale")
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+
+ var actualValue = progress
+ dataStore.saveThreshold(actualValue);
+ Settings.threshold = actualValue;
+ thresholdText.text = String.format("%.2f", actualValue/100f)
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {}
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {}
+ })
+
}
companion object {
var soundClassifier: SoundClassifier? = null
@@ -57,6 +139,34 @@ class MainActivity : AppCompatActivity() {
// }
}
+
+ @SuppressLint("SetTextI18n")
+ override fun onResume() {
+ super.onResume()
+ // Start periodic updates using coroutines
+ updateJob = lifecycleScope.launch {
+ while (isActive) { // isActive is a property of the coroutine scope
+ updateCounter++
+ if (last_message_time == 0.toLong())
+ {
+ last_message_delay.text = "No messages received"
+ } else
+ {
+ last_message_delay.text = "Last message: ${(Instant.now().toEpochMilli() - last_message_time)/1000F.toInt()} seconds ago"
+ }
+
+// last_message_delay.text = "Update count: $updateCounter"
+ delay(500) // Update every 1 second
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ // Cancel the coroutine when activity is not visible
+ updateJob?.cancel()
+ }
+
private fun requestPermissions() {
val perms = mutableListOf()
diff --git a/mobile/src/main/java/com/birdsounds/identify/phone_MessageListenerService.kt b/mobile/src/main/java/com/birdsounds/identify/phone_MessageListenerService.kt
index 2bae8b1..6de6020 100644
--- a/mobile/src/main/java/com/birdsounds/identify/phone_MessageListenerService.kt
+++ b/mobile/src/main/java/com/birdsounds/identify/phone_MessageListenerService.kt
@@ -10,6 +10,19 @@ import decodeAACToPCM
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
+import java.time.Instant
+
+
+var last_message_time = 0L;
+fun ByteArray.toLong(): Long {
+ require(size <= 8) { "ByteArray too large to fit in Long" }
+
+ var result = 0L
+ for (byte in this) {
+ result = (result shl 8) or (byte.toLong() and 0xFF)
+ }
+ return result
+}
class MessageListenerService : WearableListenerService() {
@@ -30,6 +43,10 @@ class MessageListenerService : WearableListenerService() {
}
var tstamp_bytes = p0.data.copyOfRange(0, Long.SIZE_BYTES)
+
+ last_message_time = tstamp_bytes.toLong()
+
+
var audio_bytes_og = p0.data.copyOfRange(Long.SIZE_BYTES, p0.data.size)
@@ -63,8 +80,12 @@ class MessageListenerService : WearableListenerService() {
var sorted_list = soundclassifier.executeScoring(short_array)
Log.w(TAG, "FINISHED SCORING");
Log.w(TAG, "")
- for (i in 0 until 5) {
+ val threshold = Settings.threshold/100f
+ for (i in 0 until 10) {
val score = sorted_list[i].value
+ if (score < threshold) {
+ continue
+ }
val index = sorted_list[i].index
val species_name = soundclassifier.labelList[index]
Log.w(TAG, species_name + ", " + score.toString())
diff --git a/mobile/src/main/java/com/birdsounds/identify/phone_Settings.kt b/mobile/src/main/java/com/birdsounds/identify/phone_Settings.kt
index 2334192..2fd398e 100644
--- a/mobile/src/main/java/com/birdsounds/identify/phone_Settings.kt
+++ b/mobile/src/main/java/com/birdsounds/identify/phone_Settings.kt
@@ -1,9 +1,11 @@
package com.birdsounds.identify
-class Settings {
+object Settings {
var local_model_file: String = "2024_08_16_audio_model.tflite"
var pkg_model_file: String = "2024_08_16/audio-model.tflite"
var local_meta_model_file: String = "2024_08_16_meta_model.tflite"
var pkg_meta_model_file: String = "2024_08_16/meta-model.tflite"
+
+ var threshold: Int = 50;
}
diff --git a/mobile/src/main/java/com/birdsounds/identify/phone_SoundClassifier.kt b/mobile/src/main/java/com/birdsounds/identify/phone_SoundClassifier.kt
index a850bb6..e3c6207 100644
--- a/mobile/src/main/java/com/birdsounds/identify/phone_SoundClassifier.kt
+++ b/mobile/src/main/java/com/birdsounds/identify/phone_SoundClassifier.kt
@@ -26,6 +26,8 @@ import uk.me.berndporr.iirj.Butterworth
import kotlin.math.sin
+
+var local_species = ""
class SoundClassifier(
context: Context,
private val options: Options = Options()
@@ -81,7 +83,7 @@ class SoundClassifier(
/** Number of output classes of the TFLite model. */
private var modelNumClasses = 0
private var metaModelNumClasses = 0
- private var settings: Settings = Settings();
+ private var settings = Settings;
/** Used to hold the real-time probabilities predicted by the model for the output classes. */
private lateinit var predictionProbs: FloatArray
@@ -274,6 +276,17 @@ class SoundClassifier(
metaOutputBuffer.rewind()
metaOutputBuffer.get(metaPredictionProbs) // Copy data to metaPredictionProbs.
+ var cloned = metaPredictionProbs.clone()
+
+ val sorted_indices = cloned.withIndex().sortedByDescending { it.value }.map{it.index}
+
+ local_species = "Most likely:\n"
+ for (i in 0..5) {
+ local_species += " " +labelList[sorted_indices[i]].split("_")[1]
+ local_species += "\n";
+ }
+
+
for (i in metaPredictionProbs.indices) {
metaPredictionProbs[i] =
diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml
index 86a5d97..93eab34 100644
--- a/mobile/src/main/res/layout/activity_main.xml
+++ b/mobile/src/main/res/layout/activity_main.xml
@@ -7,13 +7,89 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
index fea393f..16037a6 100644
--- a/wear/build.gradle.kts
+++ b/wear/build.gradle.kts
@@ -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)
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index 8f403bd..d7283c1 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -1,7 +1,9 @@
+
+
@@ -26,6 +28,8 @@
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_Composables.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_Composables.kt
new file mode 100644
index 0000000..0d1a454
--- /dev/null
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_Composables.kt
@@ -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
+ )
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_ControlDashboard.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_ControlDashboard.kt
deleted file mode 100644
index 93d3b93..0000000
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_ControlDashboard.kt
+++ /dev/null
@@ -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
-
- }
-}
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainActivity.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainActivity.kt
index 06c6ea2..4f58cbb 100644
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainActivity.kt
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainActivity.kt
@@ -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("")
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
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")
+ },
+
+ )
+
+
+ }
+ }
}
}
-
}
}
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainState.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainState.kt
index f9c1253..ab54e6d 100644
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainState.kt
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_MainState.kt
@@ -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) {
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_MessageListenerService.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_MessageListenerService.kt
index 9bfd65e..fc72cf8 100644
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_MessageListenerService.kt
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_MessageListenerService.kt
@@ -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 = 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)
}
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_Other.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_Other.kt
new file mode 100644
index 0000000..811c7cb
--- /dev/null
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_Other.kt
@@ -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.
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_SoundRecorder.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_SoundRecorder.kt
index 2a91f33..e9d81fb 100644
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_SoundRecorder.kt
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_SoundRecorder.kt
@@ -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
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesList.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesList.kt
index c19bb97..646dfea 100644
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesList.kt
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesList.kt
@@ -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 = _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()
object SpeciesList {
- var internal_list = mutableListOf()
+ var trigger_redraw = 0;
+ var num_new_entries = 0;
var do_add_observation = false
var _list_on_ui: SnapshotStateList>? = null
fun setSpeciecsShow_list(list_in: SnapshotStateList>) {
_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)
+
}
+
}
\ No newline at end of file
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesListView.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesListView.kt
index f67a0e4..75587c4 100644
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesListView.kt
+++ b/wear/src/main/java/com/birdsounds/identify/presentation/watch_SpeciesListView.kt
@@ -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>()
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalHorologistApi::class)
@Composable
-fun SpeciesListView(context: Context,
+fun SpeciesListView(
+ context: Context, appState: AppState,
) {
- val species_list_show = mutableStateListOf>()
- 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> = 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)
diff --git a/wear/src/main/java/com/birdsounds/identify/presentation/watch_StartRecordingScreen.kt b/wear/src/main/java/com/birdsounds/identify/presentation/watch_StartRecordingScreen.kt
deleted file mode 100644
index 9b01bff..0000000
--- a/wear/src/main/java/com/birdsounds/identify/presentation/watch_StartRecordingScreen.kt
+++ /dev/null
@@ -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(
- listOf(
- AppState.Recording,
- AppState.Ready
- )
-)
diff --git a/wear/src/main/res/values/styles.xml b/wear/src/main/res/values/styles.xml
index dbf9c83..eda7b00 100644
--- a/wear/src/main/res/values/styles.xml
+++ b/wear/src/main/res/values/styles.xml
@@ -2,7 +2,7 @@
\ No newline at end of file