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

@@ -4,18 +4,29 @@
<selectionStates>
<SelectionState runConfigName="mobile">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="wear">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-03-08T17:32:57.542834800Z">
<DropdownSelection timestamp="2025-03-09T15:35:16.375287800Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.195:35507;connection=7a90992e" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFAX803LMFR" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="wear">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-03-29T14:49:45.062493500Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\thebears\.android\avd\Wear_OS_Large_Round_API_34.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="MainActivity">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.20" />
<option name="version" value="2.1.10" />
</component>
</project>

View File

@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidLayouts">
<shared>
<config />
</shared>
<layouts>
<layout url="file://$PROJECT_DIR$/wear/src/main/res/layout/layout.xml">
<config>
<theme>@android:style/Theme.DeviceDefault</theme>
</config>
</layout>
</layouts>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="e1c82fb5-3f9e-43af-a0be-9f1954a6c1b9" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/Downloader.java" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/Downloader.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/SoundClassifier.kt" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/SoundClassifier.kt" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[]" />
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$">
<ProjectState />
</projectState>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Class" />
<option value="Kotlin Class" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="2toSwb3riPZMH7ufWxT9aBYCKaO" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Android App.mobile.executor&quot;: &quot;Run&quot;,
&quot;Android App.wear.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.readMode.enableVisualFormatting&quot;: &quot;true&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;com.google.services.firebase.aqiPopupShown&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/isp/Seafile/Designs/Android/bird_sound_identify_wearos&quot;,
&quot;project.structure.last.edited&quot;: &quot;Modules&quot;,
&quot;project.structure.proportion&quot;: &quot;0.17&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.2&quot;
}
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\isp\Seafile\Designs\Android\bird_sound_identify_wearos\mobile\src\main\assets\2024_08_16" />
</key>
<key name="MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY">
<recent name="com.birdsounds.identify.presentation" />
</key>
</component>
<component name="RunManager" selected="Android App.mobile">
<configuration name="mobile" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="identify.mobile" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="wear" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="identify.wear" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="e1c82fb5-3f9e-43af-a0be-9f1954a6c1b9" name="Changes" comment="" />
<created>1741017174616</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1741017174616</updated>
</task>
<servers />
</component>
<component name="play_dynamic_filters_status">
<option name="appIdToCheckInfo">
<map>
<entry key="com.birdsounds.identify">
<value>
<CheckInfo lastCheckTimestamp="1741381679027" />
</value>
</entry>
<entry key="com.birdsounds.identify.test">
<value>
<CheckInfo lastCheckTimestamp="1741381679027" />
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidLayouts">
<shared>
<config />
</shared>
<layouts>
<layout url="file://$PROJECT_DIR$/wear/src/main/res/layout/layout.xml">
<config>
<theme>@android:style/Theme.DeviceDefault</theme>
</config>
</layout>
</layouts>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="e1c82fb5-3f9e-43af-a0be-9f1954a6c1b9" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/Downloader.java" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/Downloader.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/SoundClassifier.kt" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/java/com/birdsounds/identify/SoundClassifier.kt" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[]" />
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$">
<ProjectState />
</projectState>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Class" />
<option value="Kotlin Class" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="2toSwb3riPZMH7ufWxT9aBYCKaO" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Android App.mobile.executor&quot;: &quot;Run&quot;,
&quot;Android App.wear.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.readMode.enableVisualFormatting&quot;: &quot;true&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;com.google.services.firebase.aqiPopupShown&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/isp/Seafile/Designs/Android/bird_sound_identify_wearos&quot;,
&quot;project.structure.last.edited&quot;: &quot;Modules&quot;,
&quot;project.structure.proportion&quot;: &quot;0.17&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.2&quot;
}
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\isp\Seafile\Designs\Android\bird_sound_identify_wearos\mobile\src\main\assets\2024_08_16" />
</key>
<key name="MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY">
<recent name="com.birdsounds.identify.presentation" />
</key>
</component>
<component name="RunManager" selected="Android App.mobile">
<configuration name="mobile" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="identify.mobile" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="wear" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="identify.wear" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="e1c82fb5-3f9e-43af-a0be-9f1954a6c1b9" name="Changes" comment="" />
<created>1741017174616</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1741017174616</updated>
</task>
<servers />
</component>
<component name="play_dynamic_filters_status">
<option name="appIdToCheckInfo">
<map>
<entry key="com.birdsounds.identify">
<value>
<CheckInfo lastCheckTimestamp="1741381679027" />
</value>
</entry>
<entry key="com.birdsounds.identify.test">
<value>
<CheckInfo lastCheckTimestamp="1741381679027" />
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -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" }

View File

@@ -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)

View File

@@ -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;

View File

@@ -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<Address>? =
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!!
)
}

View File

@@ -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<Preferences> 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<TextView>(R.id.threshold_value_text)
val seekBar = findViewById<SeekBar>(R.id.threshold_set_scale_bar)
last_message_delay = findViewById<TextView>(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<String>()

View File

@@ -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())

View File

@@ -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;
}

View File

@@ -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] =

View File

@@ -7,13 +7,89 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:paddingTop="15dp"
android:text="Threshold:"
android:textSize="34sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="55dp"
android:orientation="horizontal">
<TextView
android:id="@+id/threshold_value_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="4"
android:gravity="center"
android:text="Threshold:"
android:textAlignment="center"
android:textSize="28sp" />
<SeekBar
android:id="@+id/threshold_set_scale_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center|fill_horizontal|fill_vertical"
android:layout_weight="1"
android:max="100"
android:min="0" />
</LinearLayout>
<TextView
android:id="@+id/location_string"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:paddingTop="15dp"
android:paddingRight="5dp"
android:text="Location:"
android:textSize="34sp" />
<TextView
android:id="@+id/location_lat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:paddingTop="15dp"
android:paddingRight="5dp"
android:text="Latitude:"
android:textSize="20sp" />
<TextView
android:id="@+id/location_long"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:text="Longitude:"
android:textSize="20sp" />
<TextView
android:id="@+id/local_species"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:text="Local Species:"
android:textSize="20sp" />
<TextView
android:id="@+id/last_message_delay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Last Message" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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>