Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e73d52d221 | |||
| 7ff235fb3f | |||
| bef71f8d00 | |||
| 06b98f107e | |||
| 65380da39a | |||
| bb17c0651e | |||
| 94196692c8 |
1
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
29
.idea/deploymentTargetSelector (SFConflict ispatel 2025-03-03-12-04-56).xml
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="wear">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-03-03T16:57:28.073351600Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFAX803LMFR" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="mobile">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-03-03T16:57:37.023647400Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=569cc5b0" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
15
.idea/deploymentTargetSelector.xml
generated
@@ -2,28 +2,31 @@
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="wear">
|
||||
<SelectionState runConfigName="mobile">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-10T00:57:13.348042Z">
|
||||
<DropdownSelection timestamp="2025-03-09T15:35:16.375287800Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/isp/.android/avd/Wear_OS_Large_Round_API_34.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFAX803LMFR" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="mobile">
|
||||
<SelectionState runConfigName="wear">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-10T01:00:38.270417Z">
|
||||
<DropdownSelection timestamp="2025-03-29T14:49:45.062493500Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/isp/.android/avd/Wear_OS_Large_Round_API_34.avd" />
|
||||
<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
@@ -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>
|
||||
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
||||
260
.idea/workspace (SFConflict ispatel 2025-03-03-12-04-56).xml
generated
Normal file
@@ -0,0 +1,260 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidLayouts">
|
||||
<shared>
|
||||
<config />
|
||||
</shared>
|
||||
</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 afterPath="$PROJECT_DIR$/mobile/src/main/ic_launcher-playstore.png" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/deploymentTargetSelector.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/deploymentTargetSelector.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/gradle/libs.versions.toml" beforeDir="false" afterPath="$PROJECT_DIR$/gradle/libs.versions.toml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/build.gradle.kts" afterDir="false" />
|
||||
<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/res/drawable/ic_launcher_background.xml" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/drawable/ic_launcher_background.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-anydpi/ic_launcher.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-anydpi/ic_launcher_round.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp" beforeDir="false" afterPath="$PROJECT_DIR$/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/wear/src/main/res/mipmap-hdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/wear/src/main/res/mipmap-hdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/wear/src/main/res/mipmap-mdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/wear/src/main/res/mipmap-mdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/wear/src/main/res/mipmap-xhdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/wear/src/main/res/mipmap-xhdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/wear/src/main/res/mipmap-xxhdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/wear/src/main/res/mipmap-xxhdpi/ic_launcher.webp" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.webp" beforeDir="false" afterPath="$PROJECT_DIR$/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.webp" 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[DeviceId(pluginId=PhysicalDevice, isTemplate=false, identifier=serial=569cc5b0)]" />
|
||||
<component name="ExternalProjectsData">
|
||||
<projectState path="$PROJECT_DIR$">
|
||||
<ProjectState />
|
||||
</projectState>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="2toSwb3riPZMH7ufWxT9aBYCKaO" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}]]></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="1741019799252" />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="com.birdsounds.identify.test">
|
||||
<value>
|
||||
<CheckInfo lastCheckTimestamp="1741019799252" />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
254
.idea/workspace (SFConflict ispatel 2025-03-10-10-03-26).xml
generated
Normal 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">{
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="2toSwb3riPZMH7ufWxT9aBYCKaO" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"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"
|
||||
}
|
||||
}</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>
|
||||
254
.idea/workspace (SFConflict ispatel@live.com 2025-03-08-01-11-41).xml
generated
Normal 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">{
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="2toSwb3riPZMH7ufWxT9aBYCKaO" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"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"
|
||||
}
|
||||
}</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>
|
||||
@@ -1,4 +0,0 @@
|
||||
kotlin version: 2.0.20
|
||||
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||
1. Kotlin compile daemon is ready
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
[versions]
|
||||
agp = "8.8.0"
|
||||
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"
|
||||
@@ -21,14 +36,33 @@ media3Common = "1.4.0"
|
||||
composeMaterial3 = "1.0.0-alpha23"
|
||||
workRuntimeKtx = "2.9.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
litert = "1.0.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" }
|
||||
@@ -51,10 +85,16 @@ androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-
|
||||
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "litert" }
|
||||
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" }
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.birdsounds.identify"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.birdsounds.identify"
|
||||
minSdk = 34
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.play.services.wearable)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation("uk.me.berndporr:iirj:1.7")
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.litert)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation("org.tensorflow:tensorflow-lite-gpu:2.12.0")
|
||||
implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
|
||||
implementation("org.tensorflow:tensorflow-lite-task-vision:0.4.4")
|
||||
implementation("org.tensorflow:tensorflow-lite-task-text:0.4.4")
|
||||
implementation("com.google.android.gms:play-services-tflite-gpu:16.2.0")
|
||||
implementation("com.google.android.gms:play-services-tflite-java:16.0.0-beta01")
|
||||
wearApp(project(":wear"))
|
||||
}
|
||||
@@ -40,6 +40,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(fileTree("libs") {
|
||||
include("*.jar")
|
||||
})
|
||||
api(files("libs/opus.aar"))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.play.services.wearable)
|
||||
@@ -51,11 +55,17 @@ 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.litert)
|
||||
implementation(libs.androidx.datastore.core.android)
|
||||
implementation(libs.core.ktx)
|
||||
// implementation(libs.litert)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
BIN
mobile/libs/opus.aar
Normal file
BIN
mobile/src/main/assets/2024_08_16/audio-model.tflite
Normal file
6522
mobile/src/main/assets/2024_08_16/en_us.txt
Normal file
BIN
mobile/src/main/assets/2024_08_16/meta-model.tflite
Normal file
BIN
mobile/src/main/assets/metamodel_FP16.tflite
Normal file
BIN
mobile/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 507 KiB |
@@ -1,189 +0,0 @@
|
||||
package com.birdsounds.identify;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public class Downloader {
|
||||
static final String modelFILE = "model.tflite";
|
||||
static final String metaModelFILE = "metaModel.tflite";
|
||||
static final String modelURL = "https://raw.githubusercontent.com/woheller69/whoBIRD-TFlite/master/BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite";
|
||||
static final String model32URL = "https://raw.githubusercontent.com/woheller69/whoBIRD-TFlite/master/BirdNET_GLOBAL_6K_V2.4_Model_FP32.tflite";
|
||||
static final String metaModelURL = "https://raw.githubusercontent.com/woheller69/whoBIRD-TFlite/master/BirdNET_GLOBAL_6K_V2.4_MData_Model_FP16.tflite";
|
||||
static final String modelMD5 = "b1c981fe261910b473b9b7eec9ebcd4e";
|
||||
static final String model32MD5 = "6c7c42106e56550fc8563adb31bc120e";
|
||||
static final String metaModelMD5 ="f1a078ae0f244a1ff5a8f1ccb645c805";
|
||||
|
||||
public static boolean checkModels(final Activity activity) {
|
||||
File modelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + modelFILE);
|
||||
File metaModelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + metaModelFILE);
|
||||
String calcModelMD5 = "";
|
||||
String calcMetaModelMD5 = "";
|
||||
if (modelFile.exists()) {
|
||||
try {
|
||||
byte[] data = Files.readAllBytes(Paths.get(modelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (metaModelFile.exists()) {
|
||||
try {
|
||||
byte[] data = Files.readAllBytes(Paths.get(metaModelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcMetaModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (modelFile.exists() && !(calcModelMD5.equals(modelMD5) || calcModelMD5.equals(model32MD5))) modelFile.delete();
|
||||
if (metaModelFile.exists() && !calcMetaModelMD5.equals(metaModelMD5)) metaModelFile.delete();
|
||||
|
||||
return (calcModelMD5.equals(modelMD5) || calcModelMD5.equals(model32MD5)) && calcMetaModelMD5.equals(metaModelMD5);
|
||||
}
|
||||
|
||||
public static void downloadModels(final Activity activity) {
|
||||
File modelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + modelFILE);
|
||||
Log.d("Heyy","Model file checking");
|
||||
if (!modelFile.exists()) {
|
||||
Log.d("whoBIRD", "model file does not exist");
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
URL url;
|
||||
if (false) url = new URL(model32URL);
|
||||
else url = new URL(modelURL);
|
||||
|
||||
Log.d("whoBIRD", "Download model");
|
||||
|
||||
URLConnection ucon = url.openConnection();
|
||||
ucon.setReadTimeout(5000);
|
||||
ucon.setConnectTimeout(10000);
|
||||
|
||||
InputStream is = ucon.getInputStream();
|
||||
BufferedInputStream inStream = new BufferedInputStream(is, 1024 * 5);
|
||||
|
||||
modelFile.createNewFile();
|
||||
|
||||
FileOutputStream outStream = new FileOutputStream(modelFile);
|
||||
byte[] buff = new byte[5 * 1024];
|
||||
|
||||
int len;
|
||||
while ((len = inStream.read(buff)) != -1) {
|
||||
outStream.write(buff, 0, len);
|
||||
}
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
inStream.close();
|
||||
|
||||
String calcModelMD5="";
|
||||
if (modelFile.exists()) {
|
||||
byte[] data = Files.readAllBytes(Paths.get(modelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} else {
|
||||
throw new IOException(); //throw exception if there is no modelFile at this point
|
||||
}
|
||||
|
||||
if (!(calcModelMD5.equals(modelMD5) || calcModelMD5.equals(model32MD5) )){
|
||||
modelFile.delete();
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
} else {
|
||||
activity.runOnUiThread(() -> {
|
||||
|
||||
});
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | IOException i) {
|
||||
activity.runOnUiThread(() -> Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show());
|
||||
modelFile.delete();
|
||||
Log.w("whoBIRD", activity.getResources().getString(R.string.error_download), i);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
} else {
|
||||
Log.d("whoBIRD","model exists");
|
||||
activity.runOnUiThread(() -> {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
File metaModelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + metaModelFILE);
|
||||
if (!metaModelFile.exists()) {
|
||||
Log.d("whoBIRD", "meta model file does not exist");
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
URL url = new URL(metaModelURL);
|
||||
Log.d("whoBIRD", "Download meta model");
|
||||
|
||||
URLConnection ucon = url.openConnection();
|
||||
ucon.setReadTimeout(5000);
|
||||
ucon.setConnectTimeout(10000);
|
||||
|
||||
InputStream is = ucon.getInputStream();
|
||||
BufferedInputStream inStream = new BufferedInputStream(is, 1024 * 5);
|
||||
|
||||
metaModelFile.createNewFile();
|
||||
|
||||
FileOutputStream outStream = new FileOutputStream(metaModelFile);
|
||||
byte[] buff = new byte[5 * 1024];
|
||||
|
||||
int len;
|
||||
while ((len = inStream.read(buff)) != -1) {
|
||||
outStream.write(buff, 0, len);
|
||||
}
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
inStream.close();
|
||||
|
||||
String calcMetaModelMD5="";
|
||||
if (metaModelFile.exists()) {
|
||||
byte[] data = Files.readAllBytes(Paths.get(metaModelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcMetaModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} else {
|
||||
throw new IOException(); //throw exception if there is no modelFile at this point
|
||||
}
|
||||
|
||||
if (!calcMetaModelMD5.equals(metaModelMD5)){
|
||||
metaModelFile.delete();
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
} else {
|
||||
activity.runOnUiThread(() -> {
|
||||
});
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | IOException i) {
|
||||
activity.runOnUiThread(() -> Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show());
|
||||
metaModelFile.delete();
|
||||
Log.w("whoBIRD", activity.getResources().getString(R.string.error_download), i);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
} else {
|
||||
Log.d("whoBIRD", "meta file exists");
|
||||
activity.runOnUiThread(() -> {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.birdsounds.identify;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
public class Location {
|
||||
|
||||
private static LocationListener locationListenerGPS;
|
||||
|
||||
static void stopLocation(Context context){
|
||||
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationListenerGPS!=null) locationManager.removeUpdates(locationListenerGPS);
|
||||
locationListenerGPS=null;
|
||||
}
|
||||
|
||||
static void requestLocation(Context context, SoundClassifier soundClassifier) {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && checkLocationProvider(context)) {
|
||||
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationListenerGPS==null) locationListenerGPS = new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(android.location.Location location) {
|
||||
soundClassifier.runMetaInterpreter(location);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
}
|
||||
};
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 60000, 0, locationListenerGPS);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkLocationProvider(Context context) {
|
||||
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)){
|
||||
Toast.makeText(context, "Error no GPS", Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
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 com.google.android.gms.wearable.ChannelClient
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
// private lateinit var soundClassifier: SoundClassifier
|
||||
val REQUEST_PERMISSIONS = 1337
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_main)
|
||||
Wearable.getChannelClient(this.applicationContext)
|
||||
.registerChannelCallback(object : ChannelClient.ChannelCallback() {
|
||||
override fun onChannelOpened(channel: ChannelClient.Channel) {
|
||||
super.onChannelOpened(channel)
|
||||
Log.d("HEY", "onChannelOpened")
|
||||
}
|
||||
}
|
||||
)
|
||||
Downloader.downloadModels(this)
|
||||
requestPermissions()
|
||||
soundClassifier = SoundClassifier(this, SoundClassifier.Options())
|
||||
Location.requestLocation(this, soundClassifier)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
var soundClassifier: SoundClassifier? = null
|
||||
// fun getSoundClassifier(): SoundClassifier? {
|
||||
// return soundClassifier
|
||||
// }
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
val perms = mutableListOf<String>()
|
||||
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
perms.add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
if (!perms.isEmpty()) requestPermissions(perms.toTypedArray(), REQUEST_PERMISSIONS)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Half.abs
|
||||
import android.util.Log
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.ShortBuffer
|
||||
|
||||
class MessageListenerService : WearableListenerService() {
|
||||
private val tag = "MessageListenerService"
|
||||
|
||||
// fun placeSoundClassifier(soundClassifier: SoundClassifier)
|
||||
override fun onMessageReceived(p0: MessageEvent) {
|
||||
super.onMessageReceived(p0)
|
||||
|
||||
// MainActivity
|
||||
val soundclassifier = MainActivity.soundClassifier
|
||||
if (soundclassifier == null) {
|
||||
Log.w(tag, "Have invalid sound classifier")
|
||||
return
|
||||
} else {
|
||||
Log.w(tag, "Have valid classifier")
|
||||
}
|
||||
val short_array = ShortArray(48000 * 3)
|
||||
var tstamp_bytes = p0.data.copyOfRange(0, Long.SIZE_BYTES)
|
||||
var audio_bytes = p0.data.copyOfRange(Long.SIZE_BYTES, p0.data.size)
|
||||
var string_send: String = ""
|
||||
|
||||
ByteBuffer.wrap(audio_bytes).order(
|
||||
ByteOrder.LITTLE_ENDIAN
|
||||
).asShortBuffer().get(short_array)
|
||||
Log.w(tag, short_array.sum().toString())
|
||||
var sorted_list = soundclassifier.executeScoring(short_array)
|
||||
Log.w(tag, "")
|
||||
for (i in 0 until 5) {
|
||||
val score = sorted_list[i].value
|
||||
val index = sorted_list[i].index
|
||||
val species_name = soundclassifier.labelList[index]
|
||||
Log.w(tag, species_name + ", " + score.toString())
|
||||
string_send+= species_name
|
||||
string_send+=','
|
||||
string_send+=score.toString()
|
||||
string_send+=';'
|
||||
}
|
||||
MessageSenderFromPhone.sendMessage("/audio", tstamp_bytes + string_send.toByteArray(), this)
|
||||
// Log.i(tag , short_array.map( { abs(it)}).sum().toString())
|
||||
// Log.i(tag, short_array[0].toString())
|
||||
// Log.i(tag, p0.data.toString(Charsets.US_ASCII))
|
||||
// broadcastMessage(p0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import android.content.ContentValues.TAG
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaFormat
|
||||
import android.util.Log
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
|
||||
|
||||
fun listMediaCodecDecoders() {
|
||||
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS) // Get all codecs
|
||||
val codecs = codecList.codecInfos
|
||||
|
||||
Log.e(TAG, "Available MediaCodec Decoders:")
|
||||
for (codec in codecs) {
|
||||
if (!codec.isEncoder) { // Check if the codec is a decoder
|
||||
Log.e(TAG, "Decoder: ${codec.name}")
|
||||
// List the MIME types supported by the decoder
|
||||
val supportedTypes = codec.supportedTypes
|
||||
Log.e(TAG, " Supported Types:")
|
||||
for (type in supportedTypes) {
|
||||
Log.e(TAG, " $type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun decodeAACToPCM(inputData: ByteArray): ShortArray {
|
||||
// listMediaCodecDecoders();
|
||||
// Media format configuration for AAC
|
||||
val mediaFormat = MediaFormat.createAudioFormat(
|
||||
MediaFormat.MIMETYPE_AUDIO_OPUS, // MIME type for AAC
|
||||
48000, // Sample rate, change this based on your input data
|
||||
1 // Channel count, change this based on your input data
|
||||
)
|
||||
// mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 64000) // 128kbps
|
||||
// mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 0) // AAC should use ADTS header
|
||||
// mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
||||
|
||||
// Create a decoder for AAC
|
||||
val mediaCodec = MediaCodec.createDecoderByType( MediaFormat.MIMETYPE_AUDIO_OPUS);
|
||||
mediaCodec.configure(mediaFormat, null, null, 0)
|
||||
mediaCodec.start()
|
||||
|
||||
val decodedSamples = mutableListOf<Short>()
|
||||
|
||||
// Variables for handling input and output buffers
|
||||
|
||||
val bufferInfo = MediaCodec.BufferInfo()
|
||||
|
||||
var inputOffset = 0;
|
||||
|
||||
while (inputOffset < inputData.size || true) {
|
||||
// Feed input data to the codec
|
||||
val inputBufferIndex = mediaCodec.dequeueInputBuffer(100000) // Timeout in microseconds
|
||||
if (inputBufferIndex >= 0 && inputOffset < inputData.size) {
|
||||
val inputBuffer: ByteBuffer? = mediaCodec.getInputBuffer(inputBufferIndex)
|
||||
inputBuffer?.clear()
|
||||
|
||||
// Calculate the number of bytes to write to the buffer
|
||||
val chunkSize = kotlin.math.min(inputBuffer?.capacity() ?: 0, inputData.size - inputOffset)
|
||||
Log.e(TAG, "Chunk size: " + chunkSize.toString())
|
||||
inputBuffer?.put(inputData, inputOffset, chunkSize)
|
||||
inputOffset += chunkSize
|
||||
|
||||
// Pass the data to the codec
|
||||
mediaCodec.queueInputBuffer(inputBufferIndex, 0, chunkSize, 0, 0)
|
||||
}
|
||||
|
||||
// Process output data
|
||||
val outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000)
|
||||
Log.e(TAG, "Output buffer index: " + outputBufferIndex.toString())
|
||||
if (outputBufferIndex >= 0) {
|
||||
val outputBuffer: ByteBuffer? = mediaCodec.getOutputBuffer(outputBufferIndex)
|
||||
|
||||
// Convert byte buffer to PCM data (16-bit integers)
|
||||
val pcmData = ShortArray(bufferInfo.size / 2)
|
||||
outputBuffer?.asShortBuffer()?.get(pcmData)
|
||||
|
||||
// Add PCM data to the final output array
|
||||
decodedSamples.addAll(pcmData.toList())
|
||||
|
||||
// Release the output buffer
|
||||
mediaCodec.releaseOutputBuffer(outputBufferIndex, false)
|
||||
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
// Handle format changes, if needed
|
||||
mediaCodec.outputFormat
|
||||
} else if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
// Output buffer not available, retry later
|
||||
if (inputOffset >= inputData.size) break
|
||||
}
|
||||
}
|
||||
|
||||
// Release the codec when done
|
||||
mediaCodec.stop()
|
||||
mediaCodec.release()
|
||||
|
||||
return decodedSamples.toShortArray()
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.birdsounds.identify
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
class Downloader(mainActivity: MainActivity) {
|
||||
|
||||
private val settings = Settings;
|
||||
private var activity: MainActivity = mainActivity;
|
||||
private var context: Context = activity.applicationContext;
|
||||
|
||||
|
||||
|
||||
fun copyAssetToFolder(assetName: String, destinationPath: String): Boolean {
|
||||
try {
|
||||
|
||||
// Get the input stream from the asset
|
||||
val assetInputStream = context.assets.open(assetName)
|
||||
|
||||
// Create the destination directory if it doesn't exist
|
||||
val destinationFile = File(activity.getDir("",Context.MODE_PRIVATE).absolutePath + "/" + destinationPath)
|
||||
destinationFile.parentFile?.mkdirs()
|
||||
|
||||
// Copy the file
|
||||
val buffer = ByteArray(1024)
|
||||
val outputStream = FileOutputStream(destinationFile)
|
||||
var read: Int
|
||||
|
||||
while (assetInputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
}
|
||||
|
||||
assetInputStream.close()
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareModelFiles()
|
||||
{
|
||||
copyAssetToFolder(settings.pkg_model_file, settings.local_model_file);
|
||||
copyAssetToFolder(settings.pkg_meta_model_file, settings.local_meta_model_file);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
111
mobile/src/main/java/com/birdsounds/identify/phone_Location.kt
Normal file
@@ -0,0 +1,111 @@
|
||||
@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
|
||||
|
||||
fun stopLocation(context: Context) {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
if (locationListenerGPS != null) locationManager.removeUpdates(
|
||||
locationListenerGPS!!
|
||||
)
|
||||
locationListenerGPS = null
|
||||
}
|
||||
|
||||
fun requestLocation(context: Context, soundClassifier: SoundClassifier) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED && checkLocationProvider(context)
|
||||
) {
|
||||
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()) {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
Log.w(TAG, "Sound classifier is ready");
|
||||
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("")
|
||||
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {
|
||||
}
|
||||
|
||||
override fun onProviderEnabled(provider: String) {
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
}
|
||||
}
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.PASSIVE_PROVIDER, 60000, 0f,
|
||||
locationListenerGPS!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkLocationProvider(context: Context): Boolean {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
Toast.makeText(context, "Error no GPS", Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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.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() {
|
||||
val tag = javaClass.simpleName
|
||||
return if (tag.length <= 23) tag else tag.substring(0, 23)
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
setContentView(R.layout.activity_main)
|
||||
Wearable.getChannelClient(this.applicationContext)
|
||||
.registerChannelCallback(object : ChannelClient.ChannelCallback() {
|
||||
override fun onChannelOpened(channel: ChannelClient.Channel) {
|
||||
super.onChannelOpened(channel)
|
||||
Log.d(TAG, "onChannelOpened")
|
||||
}
|
||||
}
|
||||
)
|
||||
Downloader(this).prepareModelFiles();
|
||||
Log.w(TAG, "Finished setting up downloader")
|
||||
requestPermissions()
|
||||
soundClassifier = SoundClassifier(this, SoundClassifier.Options())
|
||||
Log.w(TAG, "Starting sound classifier")
|
||||
Location.requestLocation(this, soundClassifier!!)
|
||||
Log.w(TAG, "Starting location requester")
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
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
|
||||
// fun getSoundClassifier(): SoundClassifier? {
|
||||
// return soundClassifier
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@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>()
|
||||
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
perms.add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
if (perms.isNotEmpty()) requestPermissions(perms.toTypedArray(), REQUEST_PERMISSIONS)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
package com.birdsounds.identify
|
||||
import android.content.Intent
|
||||
|
||||
object MessageConstants {
|
||||
const val intentName = "WearableMessageDisplay"
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
import com.theeasiestway.opus.Constants
|
||||
import com.theeasiestway.opus.Opus
|
||||
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() {
|
||||
|
||||
|
||||
// fun placeSoundClassifier(soundClassifier: SoundClassifier)
|
||||
override fun onMessageReceived(p0: MessageEvent) {
|
||||
super.onMessageReceived(p0)
|
||||
val codec_opus = Opus()
|
||||
codec_opus.decoderInit(Constants.SampleRate._48000(), Constants.Channels.mono())
|
||||
// MainActivity
|
||||
Log.w(TAG, "Data recv: "+p0.data.size.toString() + " bytes")
|
||||
val soundclassifier = MainActivity.soundClassifier
|
||||
if (soundclassifier == null) {
|
||||
Log.w(TAG, "Have invalid sound classifier")
|
||||
return
|
||||
} else {
|
||||
Log.w(TAG, "Have valid classifier")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
val buffer = ByteBuffer.wrap(audio_bytes_og)
|
||||
val sound_a = ByteArrayOutputStream();
|
||||
val byteArrayList = mutableListOf<ByteArray>()
|
||||
while (buffer.hasRemaining())
|
||||
{
|
||||
val num_to_read = buffer.get().toInt()
|
||||
val read_this = ByteArray(num_to_read)
|
||||
buffer.get(read_this)
|
||||
val decoded = codec_opus.decode(read_this, Constants.FrameSize._120())
|
||||
sound_a.write(decoded);
|
||||
// Log.e(TAG,"Decompressed ${read_this.size} to ${decoded?.size}")
|
||||
}
|
||||
val audio_bytes = sound_a.toByteArray()
|
||||
|
||||
codec_opus.decoderRelease();
|
||||
|
||||
val short_array = ShortArray(audio_bytes.size/2)
|
||||
// Log.e(TAG,"Size of short array buffer: "+ decoded?.size.toString());
|
||||
|
||||
ByteBuffer.wrap(audio_bytes).order(
|
||||
ByteOrder.LITTLE_ENDIAN
|
||||
).asShortBuffer().get(short_array)
|
||||
// Log.e(TAG, pcm_byte_array.sum().toString())
|
||||
Log.e(TAG, "STARTING SCORING");
|
||||
|
||||
|
||||
var string_send: String = ""
|
||||
var sorted_list = soundclassifier.executeScoring(short_array)
|
||||
Log.w(TAG, "FINISHED SCORING");
|
||||
Log.w(TAG, "")
|
||||
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())
|
||||
string_send+= species_name
|
||||
string_send+=','
|
||||
string_send+=score.toString()
|
||||
string_send+=';'
|
||||
}
|
||||
MessageSenderFromPhone.sendMessage("/audio", tstamp_bytes + string_send.toByteArray(), this)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.os.SystemClock
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.Nullable
|
||||
import org.tensorflow.lite.Interpreter
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
@@ -23,16 +23,20 @@ import kotlin.concurrent.scheduleAtFixedRate
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.cos
|
||||
import uk.me.berndporr.iirj.Butterworth
|
||||
import java.nio.ShortBuffer
|
||||
import kotlin.math.round
|
||||
import kotlin.math.sin
|
||||
|
||||
|
||||
|
||||
var local_species = ""
|
||||
class SoundClassifier(
|
||||
context: Context,
|
||||
private val options: Options = Options()
|
||||
) {
|
||||
|
||||
internal var mContext: Context
|
||||
|
||||
|
||||
|
||||
val TAG = "Sound Classifier"
|
||||
|
||||
init {
|
||||
@@ -40,14 +44,10 @@ class SoundClassifier(
|
||||
}
|
||||
|
||||
class Options(
|
||||
/** Path of the converted model label file, relative to the assets/ directory. */
|
||||
val labelsBase: String = "labels",
|
||||
/** Path of the converted .tflite file, relative to the assets/ directory. */
|
||||
val assetFile: String = "assets.txt",
|
||||
/** Path of the converted .tflite file, relative to the assets/ directory. */
|
||||
val modelPath: String = "model.tflite",
|
||||
/** Path of the meta model .tflite file, relative to the assets/ directory. */
|
||||
val metaModelPath: String = "metaModel.tflite",
|
||||
/** The required audio sample rate in Hz. */
|
||||
val sampleRate: Int = 48000,
|
||||
/** Multiplier for audio samples */
|
||||
@@ -83,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;
|
||||
|
||||
/** Used to hold the real-time probabilities predicted by the model for the output classes. */
|
||||
private lateinit var predictionProbs: FloatArray
|
||||
@@ -94,19 +94,26 @@ class SoundClassifier(
|
||||
|
||||
private var recognitionTask: TimerTask? = null
|
||||
|
||||
|
||||
/** Buffer that holds audio PCM sample that are fed to the TFLite model for inference. */
|
||||
private lateinit var inputBuffer: FloatBuffer
|
||||
private lateinit var metaInputBuffer: FloatBuffer
|
||||
|
||||
init {
|
||||
private var model_ready = false;
|
||||
init {;
|
||||
setupDecoder(context)
|
||||
loadLabels(context)
|
||||
loadAssetList(context)
|
||||
setupInterpreter(context)
|
||||
setupMetaInterpreter(context)
|
||||
warmUpModel()
|
||||
this.model_ready = true;
|
||||
}
|
||||
|
||||
fun is_model_ready(): Boolean
|
||||
{
|
||||
return this.model_ready;
|
||||
}
|
||||
private fun setupDecoder(context: Context) {
|
||||
}
|
||||
|
||||
/** Retrieve asset list from "asset_list" file */
|
||||
private fun loadAssetList(context: Context) {
|
||||
@@ -168,10 +175,12 @@ class SoundClassifier(
|
||||
|
||||
private fun setupInterpreter(context: Context) {
|
||||
try {
|
||||
val modelFilePath = context.getDir(
|
||||
"filesdir",
|
||||
val modelFilePath =
|
||||
context.getDir(
|
||||
"",
|
||||
Context.MODE_PRIVATE
|
||||
).absolutePath + "/" + options.modelPath
|
||||
).absolutePath + "/" + settings.local_model_file;
|
||||
|
||||
Log.i(TAG, "Trying to create TFLite buffer from $modelFilePath")
|
||||
val modelFile = File(modelFilePath)
|
||||
val tfliteBuffer: ByteBuffer =
|
||||
@@ -211,9 +220,9 @@ class SoundClassifier(
|
||||
|
||||
try {
|
||||
val metaModelFilePath = context.getDir(
|
||||
"filesdir",
|
||||
"",
|
||||
Context.MODE_PRIVATE
|
||||
).absolutePath + "/" + options.metaModelPath
|
||||
).absolutePath + "/" + settings.local_meta_model_file
|
||||
Log.i(TAG, "Trying to create TFLite buffer from $metaModelFilePath")
|
||||
val metaModelFile = File(metaModelFilePath)
|
||||
val tfliteBuffer: ByteBuffer =
|
||||
@@ -244,6 +253,7 @@ class SoundClassifier(
|
||||
}
|
||||
// Fill the array with 1 initially.
|
||||
metaPredictionProbs = FloatArray(metaModelNumClasses) { 1f }
|
||||
|
||||
metaInputBuffer = FloatBuffer.allocate(metaModelInputLength)
|
||||
|
||||
}
|
||||
@@ -266,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] =
|
||||
@@ -333,7 +354,6 @@ class SoundClassifier(
|
||||
else inputBuffer.put(i, butterworth.filter(s.toDouble()).toFloat())
|
||||
}
|
||||
|
||||
|
||||
inputBuffer.rewind()
|
||||
outputBuffer.rewind()
|
||||
interpreter.run(inputBuffer, outputBuffer)
|
||||
@@ -1,170 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
|
||||
@@ -7,13 +7,89 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
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" />
|
||||
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>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
mobile/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.9 KiB |
BIN
mobile/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
mobile/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 17 KiB |
BIN
mobile/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 28 KiB |
BIN
mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 33 KiB |
@@ -39,22 +39,25 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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("com.google.android.horologist:horologist-compose-layout: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")
|
||||
|
||||
api(fileTree("libs") {
|
||||
include("*.jar")
|
||||
})
|
||||
api(files("libs/opus.aar"))
|
||||
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)
|
||||
@@ -62,14 +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) // androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
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)
|
||||
|
||||
BIN
wear/libs/opus.aar
Normal 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>
|
||||
|
||||
BIN
wear/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 507 KiB |
@@ -1,67 +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,
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController) {
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
/* While this template provides a good starting point for using Wear Compose, you can always
|
||||
* take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter to find the
|
||||
* most up to date changes to the libraries and their usages.
|
||||
*/
|
||||
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
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.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
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 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
|
||||
get() {
|
||||
val tag = javaClass.simpleName
|
||||
return if (tag.length <= 23) tag else tag.substring(0, 23)
|
||||
}
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setTheme(android.R.style.Theme_DeviceDefault)
|
||||
setContent {
|
||||
WearApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun WearApp() {
|
||||
|
||||
IdentifyTheme {
|
||||
lateinit var requestPermissionLauncher: ManagedActivityResultLauncher<String, Boolean>
|
||||
val context = LocalContext.current
|
||||
val activity = context.findActivity()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
||||
val volumeViewModel: VolumeViewModel = viewModel(factory = VolumeViewModel.Factory)
|
||||
|
||||
val mainState = remember(activity) {
|
||||
MainState(activity = activity, requestPermission = {
|
||||
requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
requestPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) {
|
||||
scope.launch {
|
||||
mainState.permissionResultReturned()
|
||||
}
|
||||
}
|
||||
|
||||
val navController: NavHostController = rememberSwipeDismissableNavController()
|
||||
mainState.setNavController(navController);
|
||||
SwipeDismissableNavHost(navController = navController, startDestination = "speaker") {
|
||||
|
||||
composable("speaker") {
|
||||
StartRecordingScreen(
|
||||
context = context,
|
||||
navController = navController,
|
||||
appState = mainState.appState,
|
||||
isPermissionDenied = mainState.isPermissionDenied,
|
||||
onMicClicked = {
|
||||
scope.launch {
|
||||
mainState.onMicClicked()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable("species_list") {
|
||||
SpeciesListView(context = context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// var sdnv = SwipeDismissableNavHost(navController = navController, startDestination = "list") {
|
||||
// composable("speaker") {
|
||||
// StartRecordingScreen(
|
||||
// context = context,
|
||||
// appState = mainState.appState,
|
||||
// isPermissionDenied = mainState.isPermissionDenied,
|
||||
// onMicClicked = {
|
||||
// scope.launch {
|
||||
// mainState.onMicClicked()
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tailrec fun Context.findActivity(): Activity = when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> throw IllegalStateException("findActivity should be called in the context of an Activity")
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@Composable
|
||||
fun MessageDetail(id: String) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
ScreenScaffold(scrollState = scrollState) {
|
||||
val padding =
|
||||
ScalingLazyColumnDefaults.padding(first = ScalingLazyColumnDefaults.ItemType.Text, last = ScalingLazyColumnDefaults.ItemType.Text)()
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.rotaryWithScroll(scrollState)
|
||||
.padding(padding),
|
||||
verticalArrangement = Arrangement.Center) {
|
||||
Text(text = id, textAlign = TextAlign.Center, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@Composable
|
||||
fun MessageList(onMessageClick: (String) -> Unit) {
|
||||
val columnState =
|
||||
rememberResponsiveColumnState(contentPadding = ScalingLazyColumnDefaults.padding(first = ScalingLazyColumnDefaults.ItemType.Text,
|
||||
last = ScalingLazyColumnDefaults.ItemType.Chip))
|
||||
|
||||
ScreenScaffold(scrollState = columnState) {
|
||||
ScalingLazyColumn(columnState = columnState, modifier = Modifier.fillMaxSize()) {
|
||||
item {
|
||||
ResponsiveListHeader(contentPadding = firstItemPadding()) {
|
||||
Text(text = "Hey hey hey")
|
||||
}
|
||||
}
|
||||
item {
|
||||
Chip(label = "Message 1", onClick = { onMessageClick("message1") })
|
||||
}
|
||||
item {
|
||||
Chip(label = "Message 2", onClick = { onMessageClick("message2") })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@Composable
|
||||
fun MessageList2(onMessageClick: (String) -> Unit) {
|
||||
val columnState =
|
||||
rememberResponsiveColumnState(contentPadding = ScalingLazyColumnDefaults.padding(first = ScalingLazyColumnDefaults.ItemType.Text,
|
||||
last = ScalingLazyColumnDefaults.ItemType.Chip))
|
||||
|
||||
ScreenScaffold(scrollState = columnState) {
|
||||
ScalingLazyColumn(columnState = columnState, modifier = Modifier.fillMaxSize()) {
|
||||
item {
|
||||
ResponsiveListHeader(contentPadding = firstItemPadding()) {
|
||||
Text(text = "Hey hey hey")
|
||||
}
|
||||
}
|
||||
item {
|
||||
Chip(label = "Message 3", onClick = { onMessageClick("message3") })
|
||||
}
|
||||
item {
|
||||
Chip(label = "Message 4", onClick = { onMessageClick("message4") })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WearPreviewDevices
|
||||
@WearPreviewFontScales
|
||||
@Composable
|
||||
fun MessageDetailPreview() {
|
||||
MessageDetail("test")
|
||||
}
|
||||
|
||||
@WearPreviewDevices
|
||||
@WearPreviewFontScales
|
||||
@Composable
|
||||
fun MessageListPreview() {
|
||||
MessageList(onMessageClick = {})
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
|
||||
class MessageListenerService : WearableListenerService() {
|
||||
private val tag = "MessageListenerService"
|
||||
|
||||
override fun onMessageReceived(p0: MessageEvent) {
|
||||
super.onMessageReceived(p0)
|
||||
val t_scored = ByteBuffer.wrap(p0.data).getLong()
|
||||
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(';')
|
||||
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.05) {
|
||||
SpeciesList.add_observation(out)
|
||||
}
|
||||
}
|
||||
})
|
||||
MessageSender.messageLog.add(t_scored)
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioRecord
|
||||
import android.media.MediaRecorder
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.nio.ByteBuffer
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A helper class to provide methods to record audio input from the MIC to the internal storage.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
class SoundRecorder(
|
||||
context_in: Context,
|
||||
outputFileName: String
|
||||
) {
|
||||
|
||||
private var state = State.IDLE
|
||||
private var context = context_in
|
||||
|
||||
private enum class State {
|
||||
IDLE, RECORDING
|
||||
}
|
||||
|
||||
|
||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||
suspend fun record() {
|
||||
|
||||
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
var chunk_index: Int = 0
|
||||
val audioSource = MediaRecorder.AudioSource.DEFAULT
|
||||
val sampleRateInHz = 48000
|
||||
val channelConfig = AudioFormat.CHANNEL_IN_MONO
|
||||
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
|
||||
val buffer_size =
|
||||
4 * AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
||||
// Log.w(TAG, buffer_size.toString())
|
||||
|
||||
val bufferSizeInBytes =
|
||||
sampleRateInHz * 3 * 2 // 3 second sample, 2 bytes for each sample
|
||||
|
||||
val chunk_size = 2 * sampleRateInHz / 4 // 250ms segments, 2 bytes for each sample
|
||||
val num_chunks: Int = bufferSizeInBytes / chunk_size
|
||||
val chunked_audio_bytes = Array(num_chunks) { ByteArray(chunk_size) }
|
||||
|
||||
val audio_bytes_array = ByteArray(bufferSizeInBytes)
|
||||
val audioRecord = AudioRecord(
|
||||
/* audioSource = */ audioSource,
|
||||
/* sampleRateInHz = */ sampleRateInHz,
|
||||
/* channelConfig = */ channelConfig,
|
||||
/* audioFormat = */ audioFormat,
|
||||
/* bufferSizeInBytes = */ buffer_size
|
||||
)
|
||||
audioRecord.startRecording()
|
||||
|
||||
|
||||
val thread = Thread {
|
||||
// var sent_first: Boolean = false
|
||||
var ignore_warmup: Boolean = true
|
||||
|
||||
var num_chunked_since_last_send = 0
|
||||
var last_tstamp: Long = Instant.now().toEpochMilli()
|
||||
var do_send_message: Boolean = false
|
||||
while (true) {
|
||||
if (Thread.interrupted()) {
|
||||
// check for the interrupted flag, reset it, and throw exception
|
||||
Log.w(TAG, "Finished thread")
|
||||
break
|
||||
}
|
||||
chunk_index = chunk_index.mod(num_chunks)
|
||||
val out = audioRecord.read(
|
||||
/* audioData = */ chunked_audio_bytes[chunk_index],
|
||||
/* offsetInBytes = */ 0,
|
||||
/* sizeInBytes = */ chunk_size,
|
||||
/* readMode = */ AudioRecord.READ_BLOCKING
|
||||
)
|
||||
num_chunked_since_last_send += 1
|
||||
|
||||
do_send_message = false
|
||||
if (num_chunked_since_last_send >= num_chunks) {
|
||||
do_send_message = true
|
||||
Log.w("MSG","sending message because full 3s have passed")
|
||||
} else if ((last_tstamp in MessageSender.messageLog) && (num_chunked_since_last_send>4)) {
|
||||
do_send_message = true
|
||||
Log.w("MSG","Send message because the phone has finished")
|
||||
} else if ((ignore_warmup) && (num_chunked_since_last_send > 2)) {
|
||||
do_send_message = true
|
||||
Log.w("MSG","Sent message because ignoring warmup")
|
||||
}
|
||||
|
||||
|
||||
chunk_index += 1
|
||||
if ((do_send_message)) {
|
||||
var tstamp: Long = Instant.now().toEpochMilli()
|
||||
val tstamp_buffer = ByteBuffer.allocate(Long.SIZE_BYTES)
|
||||
val tstamp_bytes = tstamp_buffer.putLong(tstamp).array()
|
||||
var byte_send: ByteArray = ByteArray(0)
|
||||
var strr: String = ""
|
||||
for (i in 0..(num_chunks - 1)) {
|
||||
var c_index = i + chunk_index
|
||||
c_index = c_index.mod(num_chunks)
|
||||
strr += c_index.toString()
|
||||
strr += ' '
|
||||
byte_send += chunked_audio_bytes[c_index]
|
||||
}
|
||||
// do_send_message = false;
|
||||
num_chunked_since_last_send = 0
|
||||
MessageSender.messageLog.clear()
|
||||
MessageSender.sendMessage("/audio", tstamp_bytes + byte_send, context)
|
||||
last_tstamp = tstamp
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
thread.start()
|
||||
// thread.join();
|
||||
|
||||
cont.invokeOnCancellation {
|
||||
thread.interrupt()
|
||||
audioRecord.stop()
|
||||
audioRecord.release()
|
||||
state = State.IDLE
|
||||
}
|
||||
state = State.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SoundRecorder"
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
class AScore(
|
||||
_species: String,
|
||||
_score: Float,
|
||||
_timestamp: Long,
|
||||
|
||||
) {
|
||||
|
||||
var split_stuff = _species.split("_")
|
||||
val species = split_stuff[0]
|
||||
val score = _score
|
||||
val common_name = split_stuff[1]
|
||||
val timestamp = _timestamp
|
||||
fun age(): Long {
|
||||
var tstamp: Long = Instant.now().toEpochMilli()
|
||||
return (tstamp - timestamp)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
var tstamp: Long = Instant.now().toEpochMilli()
|
||||
return common_name + "," + species + "," + score.toString() + ", " + (age() / 1000.0).toString() + "s ago"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object SpeciesList {
|
||||
var internal_list = mutableListOf<AScore>()
|
||||
var do_add_observation = false
|
||||
var _list_on_ui: SnapshotStateList<MutableState<String>>? = null
|
||||
fun setSpeciecsShow_list(list_in: SnapshotStateList<MutableState<String>>) {
|
||||
_list_on_ui = list_in;
|
||||
}
|
||||
|
||||
fun add_observation(species_in: AScore) {
|
||||
Log.w(TAG,"In add obsergation")
|
||||
do_add_observation = false
|
||||
var idx = 0
|
||||
var idx_replace = -1
|
||||
for (i in internal_list) {
|
||||
if (i.species == species_in.species) {
|
||||
do_add_observation = false
|
||||
idx_replace = idx
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
if (idx_replace >= 0) {
|
||||
Log.w(TAG, "Replacing")
|
||||
internal_list[idx_replace] = species_in // _list_on_ui?.removeAt(idx_replace)
|
||||
// _list_on_ui?.add(species_in);
|
||||
// if (_list_on_ui != null) {
|
||||
// _list_on_ui?.removeAt(idx_replace)
|
||||
// _list_on_ui.add(idx_replace, species_in);
|
||||
// }
|
||||
|
||||
} else {
|
||||
internal_list.add(species_in)
|
||||
|
||||
|
||||
}
|
||||
|
||||
internal_list = internal_list.sortedBy({ (it.age()) }).toMutableList()
|
||||
|
||||
for ((index, value) in internal_list.withIndex()) {
|
||||
_list_on_ui?.get(index)?.value = value.common_name;
|
||||
}
|
||||
|
||||
Log.w(TAG, internal_list.size.toString())
|
||||
Log.w(TAG, _list_on_ui?.size.toString()) // internal_list.add(species_in)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.wear.compose.foundation.lazy.items
|
||||
import androidx.wear.compose.material.Text
|
||||
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.ScreenScaffold
|
||||
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
|
||||
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@Composable
|
||||
fun SpeciesListView(context: Context,
|
||||
) {
|
||||
val text: MutableState<String> = mutableStateOf("text")
|
||||
//text.toString()
|
||||
val species_list_show = mutableStateListOf<MutableState<String>>()
|
||||
for (i in 1..10)
|
||||
{
|
||||
val hi = mutableStateOf("hi")
|
||||
species_list_show.add(hi);
|
||||
}
|
||||
SpeciesList.setSpeciecsShow_list(species_list_show)
|
||||
val species_show: SnapshotStateList<MutableState<String>> = remember { species_list_show }
|
||||
|
||||
val columnState =
|
||||
rememberResponsiveColumnState(contentPadding = ScalingLazyColumnDefaults.padding(first = ScalingLazyColumnDefaults.ItemType.Text,
|
||||
last = ScalingLazyColumnDefaults.ItemType.Chip))
|
||||
|
||||
ScreenScaffold(scrollState = columnState) {
|
||||
ScalingLazyColumn(columnState = columnState, modifier = Modifier.fillMaxSize()) {
|
||||
items(species_show) { aSpec -> Text(text = aSpec.value)
|
||||
} // Dynamically display the chips
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +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(
|
||||
context: Context,
|
||||
appState: AppState,
|
||||
navController: NavHostController,
|
||||
isPermissionDenied: Boolean,
|
||||
onMicClicked: () -> Unit
|
||||
) {
|
||||
ScreenScaffold {
|
||||
val controlDashboardUiState = computeControlDashboardUiState(
|
||||
appState = appState,
|
||||
)
|
||||
ControlDashboard(
|
||||
controlDashboardUiState = controlDashboardUiState,
|
||||
onMicClicked = onMicClicked,
|
||||
navController = navController
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
import android.content.ContentValues.TAG
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaFormat
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Encodes PCM audio data to AAC format
|
||||
* @param pcmData The raw PCM audio data to encode
|
||||
* @param sampleRate Sample rate of the audio (e.g., 44100)
|
||||
* @param channelCount Number of audio channels (1 for mono, 2 for stereo)
|
||||
* @return ByteArray containing the encoded AAC audio data
|
||||
*/
|
||||
|
||||
|
||||
fun listMediaCodecEncoders() {
|
||||
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS) // List all codecs
|
||||
val codecs = codecList.codecInfos
|
||||
|
||||
println("Available MediaCodec Encoders:")
|
||||
for (codec in codecs) {
|
||||
if (codec.isEncoder) {
|
||||
Log.e(TAG, "Encoder: ${codec.name}")
|
||||
// List supported types for this encoder
|
||||
val supportedTypes = codec.supportedTypes
|
||||
Log.w(TAG, " Supported Types:")
|
||||
for (type in supportedTypes) {
|
||||
Log.w(TAG, " $type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun encodePcmToAac(pcmData: ByteArray): ByteArray {
|
||||
// Create a format for the encoder
|
||||
var sampleRate = 48000
|
||||
var channelCount = 1
|
||||
// listMediaCodecEncoders();
|
||||
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, sampleRate, channelCount)
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, 64000) // 128kbps
|
||||
// format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
||||
// format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, pcmData.size)
|
||||
|
||||
// Create and configure the encoder
|
||||
val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_OPUS)
|
||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
codec.start()
|
||||
|
||||
// Use ByteArrayOutputStream to collect encoded data
|
||||
val encodedBytes = mutableListOf<Byte>()
|
||||
val bufferInfo = MediaCodec.BufferInfo()
|
||||
|
||||
var allInputSubmitted = false
|
||||
var inputOffset = 0
|
||||
var presentationTimeUs = 0L
|
||||
val frameSize = 1024 * channelCount * 2 // Typical frame size for AAC encoding (1024 samples, 16-bit PCM)
|
||||
|
||||
try {
|
||||
while (!allInputSubmitted || bufferInfo.flags != MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
|
||||
// Submit input data to encoder
|
||||
if (!allInputSubmitted) {
|
||||
val inputBufferId = codec.dequeueInputBuffer(10000)
|
||||
if (inputBufferId >= 0) {
|
||||
val inputBuffer = codec.getInputBuffer(inputBufferId)
|
||||
inputBuffer?.clear()
|
||||
|
||||
// Calculate how many bytes to read
|
||||
val bytesToRead = if (inputOffset < pcmData.size) {
|
||||
minOf(inputBuffer!!.capacity(), pcmData.size - inputOffset)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
if (bytesToRead > 0) {
|
||||
// Copy data from byte array to input buffer
|
||||
inputBuffer!!.put(pcmData, inputOffset, bytesToRead)
|
||||
inputOffset += bytesToRead
|
||||
|
||||
// Calculate presentation time in microseconds
|
||||
// (samples / sample rate) * 1_000_000
|
||||
val samples = bytesToRead / (2 * channelCount) // 16-bit samples
|
||||
presentationTimeUs += samples * 1_000_000L / sampleRate
|
||||
|
||||
codec.queueInputBuffer(
|
||||
inputBufferId,
|
||||
0,
|
||||
bytesToRead,
|
||||
presentationTimeUs,
|
||||
0
|
||||
)
|
||||
} else {
|
||||
// End of input data
|
||||
codec.queueInputBuffer(
|
||||
inputBufferId,
|
||||
0,
|
||||
0,
|
||||
presentationTimeUs,
|
||||
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
||||
)
|
||||
allInputSubmitted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get encoded data from encoder
|
||||
val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, 10000)
|
||||
when {
|
||||
outputBufferId >= 0 -> {
|
||||
val outputBuffer = codec.getOutputBuffer(outputBufferId)
|
||||
|
||||
if (outputBuffer != null && bufferInfo.size > 0) {
|
||||
// Copy encoded data to our result buffer
|
||||
val encodedChunk = ByteArray(bufferInfo.size)
|
||||
outputBuffer.position(bufferInfo.offset)
|
||||
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
|
||||
outputBuffer.get(encodedChunk)
|
||||
encodedBytes.addAll(encodedChunk.toList())
|
||||
}
|
||||
|
||||
codec.releaseOutputBuffer(outputBufferId, false)
|
||||
|
||||
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||
// You might want to store the format for muxing
|
||||
val newFormat = codec.outputFormat
|
||||
// Log.d("MediaCodec", "Output format changed: $newFormat")
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
codec.stop()
|
||||
codec.release()
|
||||
}
|
||||
return encodedBytes.toByteArray()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/* While this template provides a good starting point for using Wear Compose, you can always
|
||||
* take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter to find the
|
||||
* most up to date changes to the libraries and their usages.
|
||||
*/
|
||||
|
||||
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.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.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.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.ScreenScaffold
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
val flow_stream = MutableStateFlow<String>("")
|
||||
|
||||
val Any.TAG: String
|
||||
get() {
|
||||
val tag = javaClass.simpleName
|
||||
return if (tag.length <= 23) tag else tag.substring(0, 23)
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
|
||||
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
|
||||
val activity = context.findActivity()
|
||||
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) {
|
||||
MainState(activity = activity, requestPermission = {
|
||||
requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
})
|
||||
}
|
||||
|
||||
requestPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) {
|
||||
scope.launch {
|
||||
mainState.permissionResultReturned()
|
||||
}
|
||||
}
|
||||
|
||||
val navController: NavHostController = rememberSwipeDismissableNavController()
|
||||
mainState.setNavController(navController);
|
||||
SwipeDismissableNavHost(
|
||||
navController = navController,
|
||||
userSwipeEnabled = true,
|
||||
startDestination = "speaker"
|
||||
) {
|
||||
composable("species_list") {
|
||||
ScreenScaffold {
|
||||
SpeciesListView(context = context, appState = mainState.appState)
|
||||
}
|
||||
}
|
||||
|
||||
composable("speaker") {
|
||||
ScreenScaffold {
|
||||
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")
|
||||
},
|
||||
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
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")
|
||||
},
|
||||
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tailrec fun Context.findActivity(): Activity = when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> throw IllegalStateException("findActivity should be called in the context of an Activity")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -0,0 +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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -10,24 +10,32 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
|
||||
object MessageSender {
|
||||
const val tag = "MessageSender"
|
||||
|
||||
private val job = Job()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||
var messageLog = ConcurrentMutableSet<Long>()
|
||||
|
||||
|
||||
|
||||
|
||||
fun sendMessage(path: String, message: ByteArray, context: Context) {
|
||||
coroutineScope.launch {
|
||||
sendMessageInBackground(path, message, context)
|
||||
}
|
||||
}
|
||||
|
||||
fun compressByteArray(input: ByteArray): ByteArray {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
GZIPOutputStream(outputStream).use { gzip ->
|
||||
gzip.write(input) // Compress the byte array
|
||||
}
|
||||
return outputStream.toByteArray() // Return the compressed data
|
||||
}
|
||||
private fun sendMessageInBackground(path: String, message: ByteArray, context: Context) {
|
||||
//first get all the nodes, ie connected wearable devices.
|
||||
val nodeListTask = Wearable.getNodeClient(context).connectedNodes
|
||||
@@ -38,6 +46,19 @@ object MessageSender {
|
||||
if(nodes.isEmpty()) {
|
||||
Log.i(tag,"No Node found to send message")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// var compressed_message = audio_encoder.encodePCMToAAC(message)
|
||||
|
||||
// Log.w(tag, "Uncompressed message size "+message.size.toString())
|
||||
// Log.w(tag, "Compressed message size "+compressed_message.size.toString())
|
||||
//Now send the message to each device.
|
||||
for (node in nodes) {
|
||||
val sendMessageTask = Wearable.getMessageClient(context)
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
import com.theeasiestway.opus.Constants
|
||||
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
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
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,
|
||||
outputFileName: String
|
||||
) {
|
||||
|
||||
val codec_opus = Opus();
|
||||
private var state = State.IDLE
|
||||
private var context = context_in
|
||||
|
||||
private enum class State {
|
||||
IDLE, RECORDING
|
||||
}
|
||||
|
||||
|
||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||
suspend fun record() {
|
||||
|
||||
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
var noiseSuppressor: NoiseSuppressor? = null
|
||||
var automaticGainControl: AutomaticGainControl? = null
|
||||
var chunk_index: Int = 0
|
||||
val audioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION
|
||||
val sampleRateInHz = 48000
|
||||
val channelConfig = AudioFormat.CHANNEL_IN_MONO
|
||||
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
|
||||
|
||||
val frameSize = Constants.FrameSize._120();
|
||||
val chunkSize = frameSize.v * 2; // Mono * 2 bytes per sample
|
||||
val counts_until_reset = 3 * sampleRateInHz / frameSize.v;
|
||||
val bufferSize =
|
||||
AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
|
||||
|
||||
val audioRecord = AudioRecord(
|
||||
/* audioSource = */ audioSource,
|
||||
/* sampleRateInHz = */ sampleRateInHz,
|
||||
/* channelConfig = */ channelConfig,
|
||||
/* audioFormat = */ audioFormat,
|
||||
/* bufferSizeInBytes = */ bufferSize
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
val thread = Thread {
|
||||
// var sent_first: Boolean = false
|
||||
val outputByteStream = ByteBuffer.allocate(1000000)
|
||||
audioRecord.startRecording()
|
||||
var last_tstamp: Long = Instant.now().toEpochMilli();
|
||||
var count: Int = 0;
|
||||
while (true) /**/ {
|
||||
if (Thread.interrupted()) {
|
||||
audioRecord.release();
|
||||
Log.w(TAG, "Finished thread")
|
||||
break
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
codec_opus.encoderInit(
|
||||
Constants.SampleRate._48000(),
|
||||
Constants.Channels.mono(),
|
||||
Constants.Application.audio()
|
||||
);
|
||||
outputByteStream.clear();
|
||||
}
|
||||
|
||||
val frame = ByteArray(chunkSize)
|
||||
var offset = 0
|
||||
var remained = frame.size
|
||||
while (remained > 0) {
|
||||
val read = audioRecord.read(frame, offset, remained)
|
||||
offset += read
|
||||
remained -= read
|
||||
}
|
||||
|
||||
|
||||
val encoded: ByteArray? =
|
||||
codec_opus.encode(bytes = frame, frameSize = frameSize);
|
||||
if (encoded != null) {
|
||||
outputByteStream.put(encoded.size.toByte());
|
||||
outputByteStream.put(encoded);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
if (count == counts_until_reset) {
|
||||
Log.d(TAG, "At count reset at ${count}");
|
||||
codec_opus.encoderRelease();
|
||||
count = 0;
|
||||
val writtenBytes = outputByteStream.position()
|
||||
Log.e(TAG,"Wrote ${writtenBytes} bytes!")
|
||||
val duplicate = outputByteStream.duplicate()
|
||||
duplicate.flip() // Prepare for reading
|
||||
// outputByteStream.flip()
|
||||
val bytes = ByteArray(writtenBytes)
|
||||
duplicate.get(bytes)
|
||||
// val result = ByteArray(outputByteStream.remaining());
|
||||
var tstamp: Long = Instant.now().toEpochMilli()
|
||||
val tstamp_buffer = ByteBuffer.allocate(Long.SIZE_BYTES)
|
||||
val tstamp_bytes = tstamp_buffer.putLong(tstamp).array()
|
||||
var byte_send: ByteArray = tstamp_bytes + bytes;
|
||||
Log.e(TAG, "Sending message of 3s with size ${byte_send.size} + ${byte_send[0].toString()}")
|
||||
MessageSender.sendMessage("/audio", byte_send, context)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
thread.start()
|
||||
// thread.join();
|
||||
|
||||
cont.invokeOnCancellation {
|
||||
thread.interrupt()
|
||||
audioRecord.stop()
|
||||
audioRecord.release()
|
||||
state = State.IDLE
|
||||
}
|
||||
state = State.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SoundRecorder"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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
|
||||
|
||||
|
||||
class AScore(
|
||||
_species: String,
|
||||
_score: Float,
|
||||
_timestamp: Long,
|
||||
_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)
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
var tstamp: Long = Instant.now().toEpochMilli()
|
||||
return common_name + "," + species + "," + score.toString() + ", " + (age() / 1000.0).toString() + "s ago"
|
||||
}
|
||||
}
|
||||
|
||||
var internal_list = mutableListOf<AScore>()
|
||||
|
||||
object SpeciesList {
|
||||
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): 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
|
||||
}
|
||||
idx += 1
|
||||
}
|
||||
if (idx_replace >= 0) {
|
||||
Log.w(TAG, "Replacing")
|
||||
internal_list[idx_replace] = species_in
|
||||
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()
|
||||
|
||||
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)))
|
||||
}
|
||||
val time_stamp = System.currentTimeMillis()
|
||||
for ((index, value) in internal_list.withIndex()) {
|
||||
_list_on_ui?.get(index)?.value = value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
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.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.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
|
||||
import androidx.compose.runtime.setValue
|
||||
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.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.wear.compose.foundation.curvedComposable
|
||||
import androidx.wear.compose.foundation.lazy.items
|
||||
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.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)
|
||||
|
||||
// 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
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
|
||||
val species_list_show = mutableStateListOf<MutableState<AScore>>()
|
||||
|
||||
@SuppressLint("UnrememberedMutableState")
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@Composable
|
||||
fun SpeciesListView(
|
||||
context: Context, appState: AppState,
|
||||
) {
|
||||
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// 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)
|
||||
// } // Dynamically display the chips
|
||||
//
|
||||
// }
|
||||
// }
|
||||
74
wear/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
5
wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
wear/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.9 KiB |
BIN
wear/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
wear/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 8.5 KiB |
BIN
wear/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 17 KiB |
BIN
wear/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 28 KiB |
BIN
wear/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 33 KiB |
@@ -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>
|
||||