Compare commits
10
Commits
main
..
efa93ab912
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa93ab912 | ||
|
|
d7f3bf386d | ||
|
|
7436599ad3 | ||
|
|
ad13a7e958 | ||
|
|
0c949a9bda | ||
|
|
415b5ef0d8 | ||
|
|
80390b09ba | ||
|
|
a956bc3564 | ||
|
|
833f8a99c1 | ||
|
|
d43982d8f7 |
+12
-328
@@ -1,331 +1,15 @@
|
|||||||
### Android ###
|
|
||||||
# Built application files
|
|
||||||
*.apk
|
|
||||||
*.ap_
|
|
||||||
*.aab
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
|
||||||
*.dex
|
|
||||||
|
|
||||||
# Java class files
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
bin/
|
|
||||||
gen/
|
|
||||||
out/
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Android Studio Navigation editor temp files
|
|
||||||
.navigation/
|
|
||||||
|
|
||||||
# Android Studio captures folder
|
|
||||||
captures/
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
*.iml
|
*.iml
|
||||||
.idea/workspace.xml
|
|
||||||
.idea/tasks.xml
|
|
||||||
.idea/gradle.xml
|
|
||||||
.idea/assetWizardSettings.xml
|
|
||||||
.idea/dictionaries
|
|
||||||
.idea/libraries
|
|
||||||
.idea/caches
|
|
||||||
|
|
||||||
# Keystore files
|
|
||||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
|
||||||
#*.jks
|
|
||||||
#*.keystore
|
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
|
||||||
.externalNativeBuild
|
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
|
||||||
google-services.json
|
|
||||||
|
|
||||||
# Freeline
|
|
||||||
freeline.py
|
|
||||||
freeline/
|
|
||||||
freeline_project_description.json
|
|
||||||
|
|
||||||
# fastlane
|
|
||||||
fastlane/report.xml
|
|
||||||
fastlane/Preview.html
|
|
||||||
fastlane/screenshots
|
|
||||||
fastlane/test_output
|
|
||||||
fastlane/readme.md
|
|
||||||
|
|
||||||
### Android Patch ###
|
|
||||||
gen-external-apklibs
|
|
||||||
|
|
||||||
### AndroidStudio ###
|
|
||||||
# Covers files to be ignored for android development using Android Studio.
|
|
||||||
|
|
||||||
# Built application files
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
|
||||||
|
|
||||||
# Java class files
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle
|
.gradle
|
||||||
|
/local.properties
|
||||||
# Signing files
|
/.idea/caches
|
||||||
.signing/
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
# Local configuration file (sdk path, etc)
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
# Proguard folder generated by Eclipse
|
/.idea/assetWizardSettings.xml
|
||||||
|
|
||||||
# Log Files
|
|
||||||
|
|
||||||
# Android Studio
|
|
||||||
/*/build/
|
|
||||||
/*/local.properties
|
|
||||||
/*/out
|
|
||||||
/*/*/build
|
|
||||||
/*/*/production
|
|
||||||
*.ipr
|
|
||||||
*~
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
# Android Patch
|
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
|
||||||
|
|
||||||
# NDK
|
|
||||||
obj/
|
|
||||||
|
|
||||||
# IntelliJ IDEA
|
|
||||||
*.iws
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# User-specific configurations
|
|
||||||
.idea/caches/
|
|
||||||
.idea/libraries/
|
|
||||||
.idea/shelf/
|
|
||||||
.idea/.name
|
|
||||||
.idea/compiler.xml
|
|
||||||
.idea/copyright/profiles_settings.xml
|
|
||||||
.idea/encodings.xml
|
|
||||||
.idea/misc.xml
|
|
||||||
.idea/modules.xml
|
|
||||||
.idea/scopes/scope_settings.xml
|
|
||||||
.idea/vcs.xml
|
|
||||||
.idea/jsLibraryMappings.xml
|
|
||||||
.idea/datasources.xml
|
|
||||||
.idea/dataSources.ids
|
|
||||||
.idea/sqlDataSources.xml
|
|
||||||
.idea/dynamic.xml
|
|
||||||
.idea/uiDesigner.xml
|
|
||||||
|
|
||||||
# OS-specific files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.DS_Store?
|
/build
|
||||||
._*
|
/captures
|
||||||
.Spotlight-V100
|
.externalNativeBuild
|
||||||
.Trashes
|
.cxx
|
||||||
ehthumbs.db
|
local.properties
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Legacy Eclipse project files
|
|
||||||
.classpath
|
|
||||||
.project
|
|
||||||
.cproject
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
|
||||||
.mtj.tmp/
|
|
||||||
|
|
||||||
# Package Files #
|
|
||||||
*.war
|
|
||||||
*.ear
|
|
||||||
|
|
||||||
# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
|
|
||||||
hs_err_pid*
|
|
||||||
|
|
||||||
## Plugin-specific files:
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/mongoSettings.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
### AndroidStudio Patch ###
|
|
||||||
|
|
||||||
!/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
### Intellij ###
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff
|
|
||||||
.idea/**/workspace.xml
|
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/**/usage.statistics.xml
|
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
|
|
||||||
### Intellij Patch ###
|
|
||||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
|
||||||
|
|
||||||
# *.iml
|
|
||||||
# modules.xml
|
|
||||||
# .idea/misc.xml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# Sonarlint plugin
|
|
||||||
.idea/sonarlint
|
|
||||||
|
|
||||||
### Java ###
|
|
||||||
# Compiled class file
|
|
||||||
|
|
||||||
# Log file
|
|
||||||
|
|
||||||
# BlueJ files
|
|
||||||
*.ctxt
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
|
||||||
|
|
||||||
# Package Files #
|
|
||||||
*.jar
|
|
||||||
*.nar
|
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
|
||||||
*.rar
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
|
||||||
|
|
||||||
### Linux ###
|
|
||||||
|
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# KDE directory preferences
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
|
||||||
.nfs*
|
|
||||||
|
|
||||||
### macOS ###
|
|
||||||
# General
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Icon must end with two \r
|
|
||||||
Icon
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.TemporaryItems
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
### Windows ###
|
|
||||||
# Windows thumbnail cache files
|
|
||||||
ehthumbs_vista.db
|
|
||||||
|
|
||||||
# Dump file
|
|
||||||
*.stackdump
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
[Dd]esktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|||||||
Generated
+1
@@ -0,0 +1 @@
|
|||||||
|
pilot
|
||||||
Generated
-123
@@ -1,123 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<code_scheme name="Project" version="173">
|
|
||||||
<JetCodeStyleSettings>
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</JetCodeStyleSettings>
|
|
||||||
<codeStyleSettings language="XML">
|
|
||||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
|
||||||
<indentOptions>
|
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
|
||||||
</indentOptions>
|
|
||||||
<arrangement>
|
|
||||||
<rules>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>xmlns:android</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>xmlns:.*</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:id</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:name</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>name</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>style</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
</rules>
|
|
||||||
</arrangement>
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
|
||||||
</component>
|
|
||||||
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.8.20" />
|
<option name="version" value="1.6.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
signingConfigs {
|
|
||||||
debug {
|
|
||||||
storeFile file('/home/gogacoder/AndroidStudioProjects/keychain/key.jks')
|
|
||||||
storePassword '123456'
|
|
||||||
keyPassword '123456'
|
|
||||||
keyAlias 'key0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
namespace 'com.listerk.helible'
|
|
||||||
compileSdk 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.listerk.helible"
|
|
||||||
minSdk 30
|
|
||||||
targetSdk 34
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled 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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("com.google.devtools.ksp").version("1.6.10-1.0.4")
|
||||||
|
id("org.jlleitschuh.gradle.ktlint").version("12.0.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.helible.pilot"
|
||||||
|
compileSdk = 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.helible.pilot"
|
||||||
|
minSdk = 28
|
||||||
|
targetSdk = 33
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.4.3"
|
||||||
|
}
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
||||||
|
implementation("androidx.activity:activity-compose:1.7.2")
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
|
||||||
|
implementation("androidx.navigation:navigation-compose:2.6.0")
|
||||||
|
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
|
||||||
|
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -4,8 +4,8 @@
|
|||||||
"type": "APK",
|
"type": "APK",
|
||||||
"kind": "Directory"
|
"kind": "Directory"
|
||||||
},
|
},
|
||||||
"applicationId": "com.listerk.helible",
|
"applicationId": "com.helible.pilot",
|
||||||
"variantName": "debug",
|
"variantName": "release",
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 1,
|
"versionCode": 1,
|
||||||
"versionName": "1.0",
|
"versionName": "1.0",
|
||||||
"outputFile": "app-debug.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elementType": "File"
|
"elementType": "File"
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.listerk.helible
|
package com.helible.pilot
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("com.listerk.helible", appContext.packageName)
|
assertEquals("com.gogacoder.testblue", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,19 +2,19 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- Legacy permissions (API 30 and below) -->
|
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
|
||||||
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" android:maxSdkVersion="30"/>-->
|
|
||||||
|
|
||||||
|
<!-- Request Bluetooth permissions for API level 31+ -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:minSdkVersion="31"
|
||||||
|
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:minSdkVersion="31"
|
||||||
|
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -24,16 +24,14 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.HeliBLE">
|
android:theme="@style/Theme.Testblue"
|
||||||
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ControlActivity"
|
android:name="com.helible.pilot.MainActivity"
|
||||||
android:exported="false" />
|
android:exported="true"
|
||||||
<activity
|
android:theme="@style/Theme.Testblue">
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
package com.helible.pilot
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class TransferFailedException : IOException("Reading incoming data failed")
|
||||||
|
|
||||||
|
class BluetoothDataTransferService(
|
||||||
|
private val socket: BluetoothSocket,
|
||||||
|
) {
|
||||||
|
fun listenForIncomingMessages(): Flow<KMessage> {
|
||||||
|
return flow {
|
||||||
|
if (!socket.isConnected)
|
||||||
|
return@flow
|
||||||
|
val buffer = ByteArray(128)
|
||||||
|
while (true) {
|
||||||
|
val byteCount: Int = try {
|
||||||
|
socket.inputStream.read(buffer)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw TransferFailedException()
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
buffer.decodeToString(
|
||||||
|
endIndex = byteCount
|
||||||
|
).toKMessage()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendMessage(bytes: ByteArray): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
socket.outputStream.write(bytes)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.helible.pilot
|
||||||
|
|
||||||
|
// Todo: add checksum
|
||||||
|
// Todo: add arguments names
|
||||||
|
|
||||||
|
data class KMessage(
|
||||||
|
val r1: UShort,
|
||||||
|
val r2: UShort,
|
||||||
|
val r3: UShort,
|
||||||
|
val emergStop: Boolean,
|
||||||
|
val alarm: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun KMessage.toByteArray(): ByteArray {
|
||||||
|
return "$$r1;$r2;$r3;$emergStop;$alarm\r\n".encodeToByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toKMessage(): KMessage {
|
||||||
|
// TODO: implement
|
||||||
|
return KMessage(
|
||||||
|
0u, 0u, 0u, emergStop = false, alarm = false
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.helible.pilot
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.helible.pilot.components.scannerScreen.BluetoothScannerScreen
|
||||||
|
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
|
||||||
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
|
import com.helible.pilot.viewmodels.SavedPreferencesImpl
|
||||||
|
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
||||||
|
import com.helible.pilot.permissions.PermissionsLauncher
|
||||||
|
import com.helible.pilot.permissions.PermissionsRequest
|
||||||
|
import com.helible.pilot.permissions.RequestHardwareFeatures
|
||||||
|
import com.helible.pilot.ui.theme.TestblueTheme
|
||||||
|
import com.helible.pilot.viewmodels.BluetoothViewModel
|
||||||
|
import com.helible.pilot.viewmodels.BluetoothViewModelFactory
|
||||||
|
import com.helible.pilot.viewmodels.PermissionDialogViewModel
|
||||||
|
import com.helible.pilot.viewmodels.PreferencesViewModel
|
||||||
|
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
// TODO: device screen logic
|
||||||
|
// TODO: constrain text size
|
||||||
|
// TODO: add Bluetooth telemetry...
|
||||||
|
// TODO: move text strings to resources
|
||||||
|
// TODO: review permissions logic
|
||||||
|
|
||||||
|
private val preferences by lazy {
|
||||||
|
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
||||||
|
}
|
||||||
|
private val preferencesViewModel by lazy {
|
||||||
|
PreferencesViewModel(preferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
val bluetoothViewModel =
|
||||||
|
viewModel<BluetoothViewModel>(factory = BluetoothViewModelFactory(applicationContext))
|
||||||
|
|
||||||
|
val permissionsViewModel = viewModel<PermissionDialogViewModel>()
|
||||||
|
val permissionLauncher = PermissionsLauncher()
|
||||||
|
permissionLauncher.setup(
|
||||||
|
onPermissionResult = { perm, isGranted ->
|
||||||
|
permissionsViewModel.onPermissionResult(perm, isGranted)
|
||||||
|
},
|
||||||
|
onGranted = { bluetoothViewModel.startScan() }
|
||||||
|
)
|
||||||
|
|
||||||
|
val bluetoothState by bluetoothViewModel.state.collectAsState()
|
||||||
|
val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = null) {
|
||||||
|
permissionLauncher.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = bluetoothState) {
|
||||||
|
if (bluetoothState.isConnected) {
|
||||||
|
Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = bluetoothState.errorMessage) {
|
||||||
|
bluetoothState.errorMessage?.let { message ->
|
||||||
|
Toast.makeText(applicationContext, "Ошибка: $message", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestblueTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
PermissionsRequest(
|
||||||
|
dismissCurrentDialog = { permissionsViewModel.dismissDialog() },
|
||||||
|
visiblePermissionDialogQueue = permissionsViewModel.visiblePermissionDialogQueue,
|
||||||
|
activity = this,
|
||||||
|
permissionLaunch = { perms -> permissionLauncher.launch(perms) }
|
||||||
|
)
|
||||||
|
|
||||||
|
RequestHardwareFeatures(
|
||||||
|
activity = this,
|
||||||
|
bluetoothUiState = bluetoothState
|
||||||
|
)
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = "device"
|
||||||
|
) {
|
||||||
|
composable("scanner") {
|
||||||
|
BluetoothScannerScreen(
|
||||||
|
bluetoothState = bluetoothState,
|
||||||
|
selectedDevice = selectedDevice,
|
||||||
|
startScan = { bluetoothViewModel.startScan() },
|
||||||
|
cancelScan = { bluetoothViewModel.cancelScan() },
|
||||||
|
choiceDevice = { device -> bluetoothViewModel.selectDevice(device) },
|
||||||
|
onScreenChanged = {
|
||||||
|
bluetoothViewModel.cancelScan()
|
||||||
|
val device = selectedDevice
|
||||||
|
if (device == null) {
|
||||||
|
preferencesViewModel.clearPreferences()
|
||||||
|
} else {
|
||||||
|
preferencesViewModel.savePreferences(
|
||||||
|
AppPreferences(
|
||||||
|
deviceName = device.name,
|
||||||
|
deviceAddress = device.macAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
navController.navigate("device")
|
||||||
|
Log.i(
|
||||||
|
"ScanActivity",
|
||||||
|
"Preferences: ${preferencesViewModel.preferences}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("device")
|
||||||
|
{
|
||||||
|
DeviceControlScreen(
|
||||||
|
bluetoothUiState = bluetoothState,
|
||||||
|
getPreferences = { preferencesViewModel.preferences },
|
||||||
|
navigateToPage = { page -> navController.navigate(page) },
|
||||||
|
connectToDevice = { device ->
|
||||||
|
bluetoothViewModel.connectToDevice(
|
||||||
|
device
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disconnectFromDevice = { bluetoothViewModel.disconnectFromDevice() },
|
||||||
|
deviceActionsList = defaultDeviceActionsList()
|
||||||
|
)
|
||||||
|
if (preferencesViewModel.preferences != null) BackHandler {}
|
||||||
|
}
|
||||||
|
composable("console")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
composable("codeblocks")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
composable("imu_calibration")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
composable("motor_test")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
composable("pid_settings")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
composable("reports")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RequiredHardwareFeatures(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
confirmButtonText: String,
|
||||||
|
featureState: Boolean,
|
||||||
|
requestFeature: () -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (!featureState) {
|
||||||
|
AlertDialog(
|
||||||
|
confirmButton = {
|
||||||
|
Divider()
|
||||||
|
TextButton(onClick = requestFeature, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(text = confirmButtonText)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = description
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = { Text(text = title) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PermissionDialog(
|
||||||
|
permissionTextProvider: PermissionTextProvider,
|
||||||
|
isPermanentDeclined: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onOkClick: () -> Unit,
|
||||||
|
onContinueClick: () -> Unit,
|
||||||
|
onGoToAppSettingsClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(text = "Требуется разрешение") },
|
||||||
|
confirmButton = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Divider()
|
||||||
|
Text(
|
||||||
|
text = if (isPermanentDeclined) {
|
||||||
|
"Выдать разрешение"
|
||||||
|
} else {
|
||||||
|
"OK"
|
||||||
|
},
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
if (isPermanentDeclined) {
|
||||||
|
onGoToAppSettingsClick()
|
||||||
|
} else {
|
||||||
|
onOkClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
if (isPermanentDeclined)
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Divider()
|
||||||
|
Text(
|
||||||
|
text = "Снова проверить наличие разрешения",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(paddingValues = PaddingValues(top = 10.dp))
|
||||||
|
.clickable { onContinueClick() },
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Unit
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = permissionTextProvider.getDescription(
|
||||||
|
isPermanentDeclined = isPermanentDeclined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionTextProvider {
|
||||||
|
fun getDescription(isPermanentDeclined: Boolean): String
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationPermissionTextProvider : PermissionTextProvider {
|
||||||
|
override fun getDescription(isPermanentDeclined: Boolean): String {
|
||||||
|
return if (isPermanentDeclined) {
|
||||||
|
"Похоже вы навсегда запретили приложению доступ к геолокации. " +
|
||||||
|
"Вы можете зайти в настройки, чтобы выдать это разрешение."
|
||||||
|
} else {
|
||||||
|
"Приложению необходимо разрешение для определения местоположения " +
|
||||||
|
"для работы с Bluetooth на устройствах с Android 11 и ниже."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BluetoothScanPermissionTextProvider : PermissionTextProvider {
|
||||||
|
override fun getDescription(isPermanentDeclined: Boolean): String {
|
||||||
|
return if (isPermanentDeclined) {
|
||||||
|
"Похоже вы навсегда запретили приложению доступ к сканированию по Bluetooth. " +
|
||||||
|
"Вы можете зайти в настройки, чтобы выдать это разрешение."
|
||||||
|
} else {
|
||||||
|
"Приложению необходимо разрешение для к сканированию по Bluetooth " +
|
||||||
|
"для работы с Bluetooth на устройствах с Android 11 и ниже"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BluetoothConnectPermissionTextProvider : PermissionTextProvider {
|
||||||
|
override fun getDescription(isPermanentDeclined: Boolean): String {
|
||||||
|
return if (isPermanentDeclined) {
|
||||||
|
"Похоже вы навсегда запретили приложению доступ к подключению по Bluetooth." +
|
||||||
|
"Вы можете зайти в настройки, чтобы выдать это разрешение."
|
||||||
|
} else {
|
||||||
|
"Приложению необходимо разрешение для к подключению по Bluetooth." +
|
||||||
|
"для работы с Bluetooth на устройствах с Android 11 и ниже"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BluetoothAdminPermissionTextProvider : PermissionTextProvider {
|
||||||
|
override fun getDescription(isPermanentDeclined: Boolean): String {
|
||||||
|
return if (isPermanentDeclined) {
|
||||||
|
"Похоже вы навсегда запретили приложению доступ к управлению настройками Bluetooth. " +
|
||||||
|
"Вы можете зайти в настройки, чтобы выдать это разрешение."
|
||||||
|
} else {
|
||||||
|
"Приложению необходимо разрешение для к управлению настройками Bluetooth " +
|
||||||
|
"для работы с Bluetooth на устройствах с Android 11 и ниже"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Title(text: String, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = modifier,
|
||||||
|
fontSize = 23.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.helible.pilot.components.deviceScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DeviceBadge(
|
||||||
|
bluetoothUiState: BluetoothUiState,
|
||||||
|
tryToReconnect: () -> Unit,
|
||||||
|
getPreferences: () -> AppPreferences?
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 15.dp),
|
||||||
|
shape = RoundedCornerShape(15)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 15.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.size(60.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
clip = true
|
||||||
|
shape = RoundedCornerShape(15)
|
||||||
|
}
|
||||||
|
.fillMaxSize()) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.helicopter),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = getPreferences()?.deviceName ?: "null",
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
DeviceConnectionStatus(bluetoothUiState)
|
||||||
|
Text(text = "Заряд батареи: 79%")
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(2.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Refresh,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(Icons.Default.Refresh.defaultWidth)
|
||||||
|
.clickable { tryToReconnect() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DeviceBadgePreview() {
|
||||||
|
DeviceBadge(
|
||||||
|
bluetoothUiState = BluetoothUiState(isConnected = true),
|
||||||
|
tryToReconnect = {},
|
||||||
|
getPreferences = {AppPreferences("Helicopter", "AA:BB:CC:FF:DD")}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.helible.pilot.components.deviceScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (bluetoothState.isConnected) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(56, 200, 35),
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(Icons.Default.CheckCircle.defaultWidth)
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
Text ("На связи")
|
||||||
|
}
|
||||||
|
else if (bluetoothState.errorMessage != null) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.cancel),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(255, 24, 35),
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(R.drawable.cancel.dp)
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
Text ("Ошибка: ${bluetoothState.errorMessage}")
|
||||||
|
}
|
||||||
|
else if (bluetoothState.isConnecting) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.sync),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(40, 123, 207),
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(R.drawable.sync.dp)
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
Text ("Подключение...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DeviceConnectionStatusPreview() {
|
||||||
|
Surface {
|
||||||
|
DeviceConnectionStatus(bluetoothState = BluetoothUiState(isConnecting = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.helible.pilot.components.deviceScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.components.Title
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DeviceControlScreen(
|
||||||
|
bluetoothUiState: BluetoothUiState,
|
||||||
|
getPreferences: () -> AppPreferences?,
|
||||||
|
navigateToPage: (String) -> Unit,
|
||||||
|
connectToDevice: (String) -> Unit,
|
||||||
|
disconnectFromDevice: () -> Unit,
|
||||||
|
deviceActionsList: Map<String, Array<Pair<String, Pair<Pair<Int, Color>, String>>>>,
|
||||||
|
scannerPageName: String = "scanner",
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val preferences: AppPreferences? = getPreferences()
|
||||||
|
if (preferences == null) {
|
||||||
|
navigateToPage(scannerPageName)
|
||||||
|
} else {
|
||||||
|
connectToDevice(preferences.deviceAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = bluetoothUiState.isEnabled) {
|
||||||
|
/* Trying to reconnect, when bluetooth is turned on */
|
||||||
|
val preferences = getPreferences()
|
||||||
|
if(preferences != null && bluetoothUiState.isEnabled)
|
||||||
|
connectToDevice(preferences.deviceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = bluetoothUiState.isLocationEnabled) {
|
||||||
|
/* Trying to reconnect, when location is turned on */
|
||||||
|
val preferences = getPreferences()
|
||||||
|
if(preferences != null && bluetoothUiState.isLocationEnabled)
|
||||||
|
connectToDevice(preferences.deviceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(5.dp)
|
||||||
|
) {
|
||||||
|
Title(
|
||||||
|
text = "Ваше устройство",
|
||||||
|
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
DeviceBadge(
|
||||||
|
bluetoothUiState = bluetoothUiState,
|
||||||
|
tryToReconnect = {
|
||||||
|
/* Trying to reconnect, when error occurred */
|
||||||
|
val preferences = getPreferences()
|
||||||
|
if(preferences != null)
|
||||||
|
connectToDevice(preferences.deviceAddress)
|
||||||
|
},
|
||||||
|
getPreferences = getPreferences
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 3.dp)) {
|
||||||
|
for (section in deviceActionsList) {
|
||||||
|
Text(section.key,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
for (action in section.value) {
|
||||||
|
TextButton(onClick = { /* TODO */}) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = action.second.first.first),
|
||||||
|
tint = action.second.first.second,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(25.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = action.second.second,
|
||||||
|
color = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = {
|
||||||
|
disconnectFromDevice()
|
||||||
|
navigateToPage(scannerPageName)
|
||||||
|
}, modifier = Modifier.padding(vertical = 10.dp)) {
|
||||||
|
Icon(painterResource(id = R.drawable.logout), contentDescription = null)
|
||||||
|
Text(
|
||||||
|
text = "Отвязать устройство",
|
||||||
|
color = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DeviceControlScreenPreview() {
|
||||||
|
Surface {
|
||||||
|
DeviceControlScreen(
|
||||||
|
bluetoothUiState = BluetoothUiState(isConnected = true),
|
||||||
|
getPreferences = { AppPreferences("Helicopter", "AA:BB:CC:DD:FF") },
|
||||||
|
navigateToPage = { /*TODO*/ },
|
||||||
|
connectToDevice = {},
|
||||||
|
disconnectFromDevice = { /*TODO*/ },
|
||||||
|
deviceActionsList = defaultDeviceActionsList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.helible.pilot.components.deviceScreen
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.helible.pilot.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun defaultDeviceActionsList(): Map<String, Array<Pair<String, Pair<Pair<Int, Color>, String>>>> {
|
||||||
|
return mapOf(
|
||||||
|
Pair(
|
||||||
|
"Управление",
|
||||||
|
arrayOf(
|
||||||
|
Pair(
|
||||||
|
"console",
|
||||||
|
Pair(
|
||||||
|
Pair(R.drawable.joystick, MaterialTheme.colorScheme.primary),
|
||||||
|
"Пульт управления"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"codeblocks",
|
||||||
|
Pair(
|
||||||
|
Pair(R.drawable.code_blocks, MaterialTheme.colorScheme.primary),
|
||||||
|
"Палитра команд"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"Настройки",
|
||||||
|
arrayOf(
|
||||||
|
Pair(
|
||||||
|
"imu_calibration",
|
||||||
|
Pair(
|
||||||
|
Pair(R.drawable.tune, MaterialTheme.colorScheme.primary),
|
||||||
|
"Калибровка гироскопа и акселерометра"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"motor_test",
|
||||||
|
Pair(
|
||||||
|
Pair(R.drawable.helicopter_icon, MaterialTheme.colorScheme.primary),
|
||||||
|
"Тестирование двигателей"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"pid_settings",
|
||||||
|
Pair(
|
||||||
|
Pair(R.drawable.controller_gen, MaterialTheme.colorScheme.primary),
|
||||||
|
"Настройки ПИД регуляторов"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"reports",
|
||||||
|
Pair(
|
||||||
|
Pair(R.drawable.construction, MaterialTheme.colorScheme.primary),
|
||||||
|
"Отчеты о полётах"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package com.helible.pilot.components.scannerScreen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.FilledIconToggleButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.compose.Dimension
|
||||||
|
import com.helible.pilot.components.Title
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Composable
|
||||||
|
fun BluetoothScannerScreen(
|
||||||
|
bluetoothState: BluetoothUiState,
|
||||||
|
selectedDevice: BluetoothDevice?,
|
||||||
|
startScan: () -> Unit,
|
||||||
|
cancelScan: () -> Unit,
|
||||||
|
choiceDevice: (device: BluetoothDevice?) -> Unit,
|
||||||
|
onScreenChanged: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val (title, devicesList, controls) = createRefs()
|
||||||
|
|
||||||
|
Title(
|
||||||
|
text = "Устройства поблизости",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
.constrainAs(title) {}
|
||||||
|
)
|
||||||
|
|
||||||
|
DiscoveredDevicesList(
|
||||||
|
bluetoothState = bluetoothState,
|
||||||
|
selectedDevice = selectedDevice,
|
||||||
|
choiceDevice = choiceDevice,
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(devicesList) {
|
||||||
|
top.linkTo(title.bottom)
|
||||||
|
bottom.linkTo(controls.top)
|
||||||
|
height = Dimension.fillToConstraints
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(5.dp)
|
||||||
|
.constrainAs(controls) {
|
||||||
|
bottom.linkTo(parent.bottom)
|
||||||
|
width = Dimension.matchParent
|
||||||
|
height = Dimension.fillToConstraints
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
FilledIconToggleButton(
|
||||||
|
checked = bluetoothState.isDiscovering,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (bluetoothState.isDiscovering) {
|
||||||
|
cancelScan()
|
||||||
|
Log.i("ScanActivity", "Trying to start scan via button")
|
||||||
|
} else {
|
||||||
|
startScan()
|
||||||
|
}
|
||||||
|
}, modifier = Modifier
|
||||||
|
.align(Alignment.Bottom)
|
||||||
|
.padding(5.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
if (bluetoothState.isDiscovering) Icons.Filled.Close
|
||||||
|
else Icons.Filled.Refresh,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onScreenChanged()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Bottom)
|
||||||
|
.padding(5.dp),
|
||||||
|
enabled = selectedDevice != null,
|
||||||
|
) {
|
||||||
|
Text(text = "Далее")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.helible.pilot.components.scannerScreen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
import com.helible.pilot.R
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Composable
|
||||||
|
fun DeviceItem(
|
||||||
|
deviceInfo: BluetoothDevice,
|
||||||
|
selectedDevice: BluetoothDevice?,
|
||||||
|
choiceDevice: (device: BluetoothDevice?) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = modifier.clickable {
|
||||||
|
choiceDevice(deviceInfo)
|
||||||
|
},
|
||||||
|
colors = CardDefaults.elevatedCardColors(
|
||||||
|
containerColor = if (deviceInfo == selectedDevice)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.padding(8.dp)) {
|
||||||
|
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
|
||||||
|
Text(
|
||||||
|
text = deviceInfo.name,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
softWrap = true
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "MAC: ${deviceInfo.macAddress}",
|
||||||
|
fontWeight = FontWeight.Thin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (deviceInfo.isScanned) {
|
||||||
|
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) {
|
||||||
|
Icon(
|
||||||
|
painterResource(id = getSignalIconForRssiValue(deviceInfo.rssi)),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(10.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSignalIconForRssiValue(rssi: Short): Int {
|
||||||
|
if (rssi >= -80) return R.drawable.signal_icon4
|
||||||
|
else if (rssi >= -90) return R.drawable.signal_icon3
|
||||||
|
else if (rssi >= -100) return R.drawable.signal_icon2
|
||||||
|
return R.drawable.signal_icon1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DeviceItemPreview() {
|
||||||
|
DeviceItem(
|
||||||
|
BluetoothDevice("Helicopter", "AA:BB:CC:DD:FF", -90, true),
|
||||||
|
null,
|
||||||
|
{_ -> },
|
||||||
|
modifier = Modifier.size(500.dp, 60.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package com.helible.pilot.components.scannerScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.components.scannerScreen.DeviceItem
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiscoveredDevicesList(
|
||||||
|
bluetoothState: BluetoothUiState,
|
||||||
|
selectedDevice: BluetoothDevice?,
|
||||||
|
choiceDevice: (device: BluetoothDevice?) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Ранее подключенные устройства",
|
||||||
|
textAlign = TextAlign.Left,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(bluetoothState.pairedBluetoothDevices) { device ->
|
||||||
|
DeviceItem(
|
||||||
|
deviceInfo = device,
|
||||||
|
selectedDevice = selectedDevice,
|
||||||
|
choiceDevice = choiceDevice,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
5.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (bluetoothState.pairedBluetoothDevices.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Нет элементов для отображения",
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Доступные устройства",
|
||||||
|
textAlign = TextAlign.Left,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(bluetoothState.scannedBluetoothDevices) { device ->
|
||||||
|
DeviceItem(
|
||||||
|
deviceInfo = device,
|
||||||
|
selectedDevice = selectedDevice,
|
||||||
|
choiceDevice = choiceDevice,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
5.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (bluetoothState.scannedBluetoothDevices.isEmpty()) {
|
||||||
|
if (bluetoothState.isDiscovering) {
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Text(text = "Поиск устройств", modifier = Modifier.padding(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Устройства поблизости не обнаружены",
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
package com.helible.pilot.controllers
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import com.helible.pilot.BluetoothDataTransferService
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
|
||||||
|
import com.helible.pilot.KMessage
|
||||||
|
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
|
||||||
|
import com.helible.pilot.receivers.BluetoothStateReceiver
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
sealed interface ConnectionResult {
|
||||||
|
object ConnectionEstablished : ConnectionResult
|
||||||
|
data class TransferSucceded(val message: KMessage) : ConnectionResult
|
||||||
|
data class Error(val message: String) : ConnectionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BluetoothController {
|
||||||
|
val isEnabled: StateFlow<Boolean>
|
||||||
|
val isLocationEnabled: StateFlow<Boolean>
|
||||||
|
val isConnected: StateFlow<Boolean>
|
||||||
|
val isScanning: StateFlow<Boolean>
|
||||||
|
val scannedDevices: StateFlow<List<BluetoothDeviceDomain>>
|
||||||
|
val pairedDevices: StateFlow<List<BluetoothDeviceDomain>>
|
||||||
|
val errors: SharedFlow<String>
|
||||||
|
|
||||||
|
fun startDiscovery()
|
||||||
|
fun cancelDiscovery()
|
||||||
|
fun connectToDevice(device: String): Flow<ConnectionResult>
|
||||||
|
suspend fun trySendMessage(message: ByteArray): Boolean
|
||||||
|
fun closeConnection()
|
||||||
|
fun onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidBluetoothController(private val context: Context) : BluetoothController {
|
||||||
|
|
||||||
|
private val bluetoothManager by lazy {
|
||||||
|
context.getSystemService(BluetoothManager::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val bluetoothAdapter by lazy {
|
||||||
|
bluetoothManager.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private val locationManager: LocationManager? by lazy {
|
||||||
|
context.getSystemService(ComponentActivity.LOCATION_SERVICE) as LocationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dataTransferService: BluetoothDataTransferService? = null
|
||||||
|
|
||||||
|
private val _isConnected: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
override val isConnected: StateFlow<Boolean>
|
||||||
|
get() = _isConnected.asStateFlow()
|
||||||
|
|
||||||
|
private val _errors = MutableSharedFlow<String>()
|
||||||
|
override val errors: SharedFlow<String>
|
||||||
|
get() = _errors.asSharedFlow()
|
||||||
|
|
||||||
|
private val _isScanning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
override val isScanning: StateFlow<Boolean>
|
||||||
|
get() = _isScanning.asStateFlow()
|
||||||
|
|
||||||
|
private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
override val isEnabled: StateFlow<Boolean>
|
||||||
|
get() = _isEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _isLocationEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
override val isLocationEnabled: StateFlow<Boolean>
|
||||||
|
get() = _isLocationEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _pairedDevices = MutableStateFlow<List<BluetoothDeviceDomain>>(emptyList())
|
||||||
|
override val pairedDevices: StateFlow<List<BluetoothDeviceDomain>>
|
||||||
|
get() = _pairedDevices.asStateFlow()
|
||||||
|
|
||||||
|
private val _scannedDevices: MutableStateFlow<List<BluetoothDeviceDomain>> =
|
||||||
|
MutableStateFlow(emptyList())
|
||||||
|
override val scannedDevices: StateFlow<List<BluetoothDeviceDomain>>
|
||||||
|
get() = _scannedDevices.asStateFlow()
|
||||||
|
|
||||||
|
private var currentClientSocket: BluetoothSocket? = null
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private val bluetoothAdapterStateReceiver = BluetoothAdapterStateReceiver(
|
||||||
|
onBluetoothEnabledChanged = { isEnabled ->
|
||||||
|
_isEnabled.update { _ -> isEnabled }
|
||||||
|
startDiscovery()
|
||||||
|
Log.i("ScanActivity", "Bluetooth enabled status: $isEnabled")
|
||||||
|
},
|
||||||
|
onDiscoveryRunningChanged = { isDiscovering ->
|
||||||
|
_isScanning.update { isDiscovering }
|
||||||
|
},
|
||||||
|
onLocationEnabledChanged = {
|
||||||
|
if (locationManager?.isLocationEnabled == true) {
|
||||||
|
_isLocationEnabled.update { true }
|
||||||
|
} else {
|
||||||
|
_isLocationEnabled.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private val bluetoothStateReceiver = BluetoothStateReceiver(
|
||||||
|
onDeviceFound = { device, rssi ->
|
||||||
|
if (!hasAllPermissions()) return@BluetoothStateReceiver
|
||||||
|
val newDevice =
|
||||||
|
BluetoothDeviceDomain(device.name ?: "null", device.address, rssi, isScanned = true)
|
||||||
|
_scannedDevices.update { devices ->
|
||||||
|
if (newDevice in devices) devices else devices + newDevice
|
||||||
|
}
|
||||||
|
Log.i(
|
||||||
|
"ScanActivity",
|
||||||
|
"Found new device: ${device.name} ${device.address} $rssi"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onConnectedStateChanged = { isConnected, device ->
|
||||||
|
if (bluetoothAdapter?.bondedDevices?.contains(device) == true) {
|
||||||
|
_isConnected.update { isConnected }
|
||||||
|
} else {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
_errors.emit("Can't connect to a non-paired device.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// SPP service UUID
|
||||||
|
const val SERVICE_UUID = "00001101-0000-1000-8000-00805F9B34FB"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
updatePairedDevices()
|
||||||
|
_isEnabled.update { bluetoothAdapter.isEnabled }
|
||||||
|
_isLocationEnabled.update { locationManager?.isLocationEnabled == true }
|
||||||
|
context.registerReceiver(
|
||||||
|
bluetoothAdapterStateReceiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||||
|
addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED)
|
||||||
|
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context.registerReceiver(
|
||||||
|
bluetoothStateReceiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||||
|
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||||
|
addAction(BluetoothDevice.ACTION_FOUND)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun startDiscovery() {
|
||||||
|
if (!hasAllPermissions()) {
|
||||||
|
Toast.makeText(context, "Ошибка: недостаточно разрешений", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!_isEnabled.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
if (locationManager?.isLocationEnabled != true) return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePairedDevices()
|
||||||
|
_scannedDevices.update { emptyList() }
|
||||||
|
|
||||||
|
if (!bluetoothAdapter.isDiscovering) {
|
||||||
|
bluetoothAdapter.startDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun cancelDiscovery() {
|
||||||
|
if (!hasAllPermissions()) return
|
||||||
|
if (bluetoothAdapter.isDiscovering) {
|
||||||
|
bluetoothAdapter.cancelDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun connectToDevice(device: String): Flow<ConnectionResult> {
|
||||||
|
if (!hasAllPermissions()) {
|
||||||
|
Toast.makeText(context, "Ошибка: нет разрешений", Toast.LENGTH_SHORT).show()
|
||||||
|
return flow {}
|
||||||
|
}
|
||||||
|
return flow {
|
||||||
|
currentClientSocket =
|
||||||
|
bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord(
|
||||||
|
UUID.fromString(SERVICE_UUID)
|
||||||
|
)
|
||||||
|
currentClientSocket?.let { socket ->
|
||||||
|
try {
|
||||||
|
socket.connect()
|
||||||
|
emit(ConnectionResult.ConnectionEstablished)
|
||||||
|
BluetoothDataTransferService(socket).also { it ->
|
||||||
|
dataTransferService = it
|
||||||
|
emitAll(
|
||||||
|
it.listenForIncomingMessages()
|
||||||
|
.map { ConnectionResult.TransferSucceded(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
closeConnection()
|
||||||
|
emit(ConnectionResult.Error("Connection was interrupted"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun trySendMessage(message: ByteArray): Boolean {
|
||||||
|
if (!hasAllPermissions()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (dataTransferService == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dataTransferService?.sendMessage(message)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeConnection() {
|
||||||
|
currentClientSocket?.close()
|
||||||
|
currentClientSocket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
context.unregisterReceiver(bluetoothAdapterStateReceiver)
|
||||||
|
context.unregisterReceiver(bluetoothStateReceiver)
|
||||||
|
closeConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun updatePairedDevices() {
|
||||||
|
if (!hasAllPermissions()) return
|
||||||
|
Log.i("ScanActivity", "${bluetoothAdapter?.bondedDevices}")
|
||||||
|
bluetoothAdapter?.bondedDevices?.onEach { device ->
|
||||||
|
_pairedDevices.update {
|
||||||
|
val currentDevice = BluetoothDeviceDomain(
|
||||||
|
name = device.name ?: "null",
|
||||||
|
macAddress = device.address,
|
||||||
|
rssi = 0,
|
||||||
|
isScanned = false
|
||||||
|
)
|
||||||
|
if (currentDevice in pairedDevices.value) {
|
||||||
|
pairedDevices.value
|
||||||
|
} else {
|
||||||
|
_pairedDevices.value + currentDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasAllPermissions(): Boolean {
|
||||||
|
val perms = if (Build.VERSION.SDK_INT <= 30) {
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
perms.forEach { perm ->
|
||||||
|
if (context.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
typealias BluetoothDeviceDomain = BluetoothDevice
|
||||||
|
|
||||||
|
data class BluetoothDevice(
|
||||||
|
val name: String,
|
||||||
|
val macAddress: String,
|
||||||
|
val rssi: Short,
|
||||||
|
val isScanned: Boolean = false,
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
data class BluetoothUiState(
|
||||||
|
val isEnabled: Boolean = false,
|
||||||
|
val isLocationEnabled: Boolean = false,
|
||||||
|
val isDiscovering: Boolean = false,
|
||||||
|
val isConnected: Boolean = false,
|
||||||
|
val isConnecting: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val scannedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
||||||
|
val pairedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RotorsSpeedMessage(val r1: Short, val r2: Short, val r3: Short)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EmergStopMessage(val emergStop: Boolean)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class AlarmStateMessage(val isAlarmOn: Boolean)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.helible.pilot.permissions
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsLauncher {
|
||||||
|
private val permissionsToRequest: Array<String> by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT <= 30) {
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var launcher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>>
|
||||||
|
|
||||||
|
@SuppressLint("ComposableNaming")
|
||||||
|
@Composable
|
||||||
|
fun setup(
|
||||||
|
onPermissionResult: (permission: String, isGranted: Boolean) -> Unit,
|
||||||
|
onGranted: () -> Unit,
|
||||||
|
) {
|
||||||
|
launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
|
onResult = { perms ->
|
||||||
|
permissionsToRequest.forEach { permission ->
|
||||||
|
onPermissionResult(permission, perms[permission] == true)
|
||||||
|
}
|
||||||
|
if (perms.values.all { it }) {
|
||||||
|
onGranted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(permissions: Array<String> = permissionsToRequest) {
|
||||||
|
launcher.launch(permissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.helible.pilot.permissions
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import com.helible.pilot.components.BluetoothAdminPermissionTextProvider
|
||||||
|
import com.helible.pilot.components.BluetoothConnectPermissionTextProvider
|
||||||
|
import com.helible.pilot.components.BluetoothScanPermissionTextProvider
|
||||||
|
import com.helible.pilot.components.LocationPermissionTextProvider
|
||||||
|
import com.helible.pilot.components.PermissionDialog
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PermissionsRequest(
|
||||||
|
visiblePermissionDialogQueue: SnapshotStateList<String>,
|
||||||
|
dismissCurrentDialog: () -> Unit,
|
||||||
|
activity: Activity,
|
||||||
|
permissionLaunch: (permissions: Array<String>) -> Unit,
|
||||||
|
) {
|
||||||
|
/* Create Dialog windows, which requests all permissions */
|
||||||
|
visiblePermissionDialogQueue.reversed()
|
||||||
|
.forEach { permission ->
|
||||||
|
PermissionDialog(
|
||||||
|
permissionTextProvider = when (permission) {
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION -> {
|
||||||
|
LocationPermissionTextProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN -> {
|
||||||
|
BluetoothScanPermissionTextProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT -> {
|
||||||
|
BluetoothConnectPermissionTextProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
Manifest.permission.BLUETOOTH_ADMIN -> {
|
||||||
|
BluetoothAdminPermissionTextProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return@forEach
|
||||||
|
},
|
||||||
|
isPermanentDeclined = !activity.shouldShowRequestPermissionRationale(permission),
|
||||||
|
onDismiss = {
|
||||||
|
if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
|
||||||
|
dismissCurrentDialog()
|
||||||
|
},
|
||||||
|
onOkClick = {
|
||||||
|
dismissCurrentDialog()
|
||||||
|
permissionLaunch(arrayOf(permission))
|
||||||
|
},
|
||||||
|
onContinueClick = {
|
||||||
|
if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
|
||||||
|
dismissCurrentDialog()
|
||||||
|
},
|
||||||
|
onGoToAppSettingsClick = {
|
||||||
|
val intent = Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", activity.packageName, null)
|
||||||
|
)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.helible.pilot.permissions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.helible.pilot.components.RequiredHardwareFeatures
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Composable
|
||||||
|
fun RequestHardwareFeatures(
|
||||||
|
activity: Activity,
|
||||||
|
bluetoothUiState: BluetoothUiState,
|
||||||
|
) {
|
||||||
|
RequiredHardwareFeatures(
|
||||||
|
title = "Включите Bluetooth",
|
||||||
|
description = "Для работы приложения требуется Bluetooth",
|
||||||
|
confirmButtonText = "Включить Bluetooth",
|
||||||
|
featureState = bluetoothUiState.isEnabled,
|
||||||
|
requestFeature = {
|
||||||
|
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
},
|
||||||
|
onDismissRequest = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
RequiredHardwareFeatures(
|
||||||
|
title = "Пожалуйста, включите геолокацию",
|
||||||
|
description = "Для работы с Bluetooth на устройствах с Android 11 и более ранних версиях, " +
|
||||||
|
"требуется геолокация.",
|
||||||
|
confirmButtonText = "Включить геолокацию",
|
||||||
|
featureState = bluetoothUiState.isLocationEnabled,
|
||||||
|
requestFeature = {
|
||||||
|
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.helible.pilot.receivers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
class BluetoothAdapterStateReceiver(
|
||||||
|
private val onBluetoothEnabledChanged: (isBluetoothEnabled: Boolean) -> Unit,
|
||||||
|
private val onDiscoveryRunningChanged: (isDiscoveryRunning: Boolean) -> Unit,
|
||||||
|
private val onLocationEnabledChanged: () -> Unit,
|
||||||
|
) : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
BluetoothAdapter.ACTION_STATE_CHANGED -> {
|
||||||
|
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
|
||||||
|
BluetoothAdapter.STATE_ON -> {
|
||||||
|
onBluetoothEnabledChanged(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.STATE_OFF -> {
|
||||||
|
onBluetoothEnabledChanged(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationManager.PROVIDERS_CHANGED_ACTION -> {
|
||||||
|
onLocationEnabledChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
|
||||||
|
onDiscoveryRunningChanged(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {
|
||||||
|
onDiscoveryRunningChanged(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.helible.pilot.receivers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
class BluetoothStateReceiver(
|
||||||
|
private val onDeviceFound: (device: BluetoothDevice, rssi: Short) -> Unit,
|
||||||
|
private val onConnectedStateChanged: (isConnected: Boolean, BluetoothDevice) -> Unit,
|
||||||
|
) : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent?.getParcelableExtra(
|
||||||
|
BluetoothDevice.EXTRA_DEVICE,
|
||||||
|
BluetoothDevice::class.java
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION") intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
}
|
||||||
|
when (intent?.action) {
|
||||||
|
BluetoothDevice.ACTION_FOUND -> {
|
||||||
|
val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE)
|
||||||
|
@SuppressLint("MissingPermission") if (device?.name != null)
|
||||||
|
onDeviceFound(device, rssi)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
||||||
|
onConnectedStateChanged(true, device ?: return)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
|
||||||
|
onConnectedStateChanged(false, device ?: return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.helible.pilot.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
|
val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
|
||||||
|
val Purple40 = Color(0xFF6650a4)
|
||||||
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
|
val Pink40 = Color(0xFF7D5260)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.helible.pilot.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Purple80,
|
||||||
|
secondary = PurpleGrey80,
|
||||||
|
tertiary = Pink80
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Purple40,
|
||||||
|
secondary = PurpleGrey40,
|
||||||
|
tertiary = Pink40
|
||||||
|
|
||||||
|
/* Other default colors to override
|
||||||
|
background = Color(0xFFFFFBFE),
|
||||||
|
surface = Color(0xFFFFFBFE),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onTertiary = Color.White,
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFF1C1B1F),
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TestblueTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
// Dynamic color is available on Android 12+
|
||||||
|
dynamicColor: Boolean = true,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
window.statusBarColor = colorScheme.primary.toArgb()
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.helible.pilot.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography = Typography(
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.helible.pilot.dataclasses.AlarmStateMessage
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.EmergStopMessage
|
||||||
|
import com.helible.pilot.dataclasses.RotorsSpeedMessage
|
||||||
|
import com.helible.pilot.controllers.BluetoothController
|
||||||
|
import com.helible.pilot.controllers.ConnectionResult
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
|
||||||
|
class BluetoothViewModel(
|
||||||
|
private val bluetoothController: BluetoothController,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _selectedDevice: MutableStateFlow<BluetoothDevice?> = MutableStateFlow(null)
|
||||||
|
val selectedDevice: StateFlow<BluetoothDevice?>
|
||||||
|
get() = _selectedDevice.asStateFlow()
|
||||||
|
|
||||||
|
private val _state: MutableStateFlow<BluetoothUiState> = MutableStateFlow(BluetoothUiState())
|
||||||
|
val state: StateFlow<BluetoothUiState> =
|
||||||
|
combine(bluetoothController.scannedDevices, bluetoothController.pairedDevices, _state)
|
||||||
|
{ scannedDevices, pairedDevices, state ->
|
||||||
|
state.copy(
|
||||||
|
scannedBluetoothDevices = scannedDevices.toList(),
|
||||||
|
pairedBluetoothDevices = pairedDevices
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
||||||
|
private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||||
|
private val rotorsStateMessegeAdapter = moshi.adapter(RotorsSpeedMessage::class.java)
|
||||||
|
private val alarmStateMessageAdapter = moshi.adapter(AlarmStateMessage::class.java)
|
||||||
|
private val emergStopMessageAdapter = moshi.adapter(EmergStopMessage::class.java)
|
||||||
|
|
||||||
|
init {
|
||||||
|
bluetoothController.isConnected.onEach { isConnected ->
|
||||||
|
_state.update { it.copy(isConnected = isConnected) }
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.errors.onEach { error ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(errorMessage = error)
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.isScanning.onEach { isDiscovering ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isDiscovering = isDiscovering,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.isEnabled.onEach { isEnabled ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.isLocationEnabled.onEach { isLocationEnabled ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLocationEnabled = isLocationEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Flow<ConnectionResult>.listen(): Job {
|
||||||
|
return onEach { result ->
|
||||||
|
when (result) {
|
||||||
|
ConnectionResult.ConnectionEstablished -> {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnected = true,
|
||||||
|
isConnecting = false,
|
||||||
|
errorMessage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ConnectionResult.TransferSucceded -> {
|
||||||
|
TODO("Telemetry not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
is ConnectionResult.Error -> {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnected = false,
|
||||||
|
isConnecting = false,
|
||||||
|
errorMessage = result.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.catch { _ ->
|
||||||
|
bluetoothController.closeConnection()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnected = false,
|
||||||
|
isConnecting = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var deviceConnectionJob: Job? = null
|
||||||
|
|
||||||
|
fun connectToDevice(device: String) {
|
||||||
|
if(_state.value.isConnected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_state.update { it.copy(isConnecting = true) }
|
||||||
|
deviceConnectionJob = bluetoothController
|
||||||
|
.connectToDevice(device)
|
||||||
|
.listen()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnectFromDevice() {
|
||||||
|
deviceConnectionJob?.cancel()
|
||||||
|
bluetoothController.closeConnection()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnecting = false,
|
||||||
|
isConnected = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectDevice(selectedDevice: BluetoothDevice?) {
|
||||||
|
_selectedDevice.update { selectedDevice }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startScan() {
|
||||||
|
selectDevice(null)
|
||||||
|
bluetoothController.startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelScan() {
|
||||||
|
bluetoothController.cancelDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
cancelScan()
|
||||||
|
bluetoothController.onDestroy()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendRotorsDutySpeed(rotorsState: RotorsSpeedMessage) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
bluetoothController.trySendMessage(
|
||||||
|
rotorsStateMessegeAdapter.toJson(rotorsState).plus("\r").toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendAlarmState(alarmStateMessage: AlarmStateMessage) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
bluetoothController.trySendMessage(
|
||||||
|
alarmStateMessageAdapter.toJson(alarmStateMessage).plus("\r").toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendEmergStop() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
bluetoothController.trySendMessage(
|
||||||
|
emergStopMessageAdapter.toJson(EmergStopMessage(true)).plus("\r").toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendR3Duty(r3: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
bluetoothController.trySendMessage(
|
||||||
|
"R3$r3\n\r".toByteArray()
|
||||||
|
)
|
||||||
|
delay(30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.helible.pilot.controllers.AndroidBluetoothController
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return BluetoothViewModel(
|
||||||
|
bluetoothController = AndroidBluetoothController(context)
|
||||||
|
) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class PermissionDialogViewModel : ViewModel() {
|
||||||
|
val visiblePermissionDialogQueue = mutableStateListOf<String>()
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
visiblePermissionDialogQueue.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPermissionResult(permission: String, isGranted: Boolean) {
|
||||||
|
if (!isGranted && !visiblePermissionDialogQueue.contains(permission)) {
|
||||||
|
visiblePermissionDialogQueue.add(permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreferencesViewModel(
|
||||||
|
private val preferencesStorage: SavedPreferences,
|
||||||
|
) : ViewModel() {
|
||||||
|
val preferences: AppPreferences? get() = preferencesStorage.getPreferences()
|
||||||
|
fun savePreferences(savedPreferences: AppPreferences) {
|
||||||
|
preferencesStorage.savePreferences(
|
||||||
|
preferences = savedPreferences
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearPreferences() {
|
||||||
|
preferencesStorage.clearPreferences()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class AppPreferences(
|
||||||
|
val deviceName: String,
|
||||||
|
val deviceAddress: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SavedPreferences {
|
||||||
|
fun getPreferences(): AppPreferences?
|
||||||
|
fun savePreferences(preferences: AppPreferences)
|
||||||
|
fun clearPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SavedPreferencesImpl(private val sharedPreferences: SharedPreferences) : SavedPreferences {
|
||||||
|
private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||||
|
private val preferencesAdapter: JsonAdapter<AppPreferences> =
|
||||||
|
moshi.adapter(AppPreferences::class.java)
|
||||||
|
|
||||||
|
override fun getPreferences(): AppPreferences? {
|
||||||
|
val json = sharedPreferences.getString("preferences", null) ?: return null
|
||||||
|
return preferencesAdapter.fromJson(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun savePreferences(preferences: AppPreferences) {
|
||||||
|
sharedPreferences.edit()
|
||||||
|
.putString("preferences", preferencesAdapter.toJson(preferences))
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearPreferences() {
|
||||||
|
sharedPreferences.edit().remove("preferences").apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.listerk.helible
|
|
||||||
|
|
||||||
object AppState {
|
|
||||||
lateinit var bleDevice: BleDevice
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.listerk.helible
|
|
||||||
|
|
||||||
data class BleDevice(val name: String, val address: String, var rssi: Int)
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.listerk.helible
|
|
||||||
|
|
||||||
//import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
//import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
//import org.w3c.dom.Text
|
|
||||||
|
|
||||||
class BleDeviceAdapter(private val devices: List<BleDevice>, private val gotoControlBtn: Button) :
|
|
||||||
RecyclerView.Adapter<BleDeviceAdapter.BleDeviceViewHolder>() {
|
|
||||||
private lateinit var itemView: View
|
|
||||||
private var selectedPosition = -1
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BleDeviceViewHolder {
|
|
||||||
itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_ble_device, parent, false)
|
|
||||||
return BleDeviceViewHolder(itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = devices.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BleDeviceViewHolder, position: Int) {
|
|
||||||
holder.nameView.text = devices[position].name
|
|
||||||
holder.addressView.text = devices[position].address
|
|
||||||
if (devices[position].rssi >= -80) holder.rssiView.setImageResource(R.drawable.signal_icon4)
|
|
||||||
else if (devices[position].rssi >= -90) holder.rssiView.setImageResource(R.drawable.signal_icon3)
|
|
||||||
else if (devices[position].rssi >= -100) holder.rssiView.setImageResource(R.drawable.signal_icon2)
|
|
||||||
else holder.rssiView.setImageResource(R.drawable.signal_icon1)
|
|
||||||
holder.cardView.isSelected = position == selectedPosition
|
|
||||||
holder.cardView.setOnClickListener {
|
|
||||||
AppState.bleDevice = devices[position]
|
|
||||||
gotoControlBtn.isEnabled = true
|
|
||||||
notifyItemChanged(selectedPosition)
|
|
||||||
selectedPosition = holder.adapterPosition
|
|
||||||
notifyItemChanged(selectedPosition)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BleDeviceViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
|
||||||
val cardView: View = itemView.rootView
|
|
||||||
val nameView: TextView = itemView.findViewById(R.id.deviceName)
|
|
||||||
val addressView: TextView = itemView.findViewById(R.id.deviceAddress)
|
|
||||||
val rssiView: ImageView = itemView.findViewById(R.id.deviceRssi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.listerk.helible
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.SeekBar
|
|
||||||
import android.widget.TextView
|
|
||||||
|
|
||||||
class ControlActivity : AppCompatActivity() {
|
|
||||||
private var sliderR1Value = 0
|
|
||||||
private var sliderR2Value = 0
|
|
||||||
private var sliderR3Value = 0
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_control)
|
|
||||||
|
|
||||||
val deviceNameText = findViewById<TextView>(R.id.controlDeviceName)
|
|
||||||
deviceNameText.text = getString(R.string.control_device_text, AppState.bleDevice.name)
|
|
||||||
|
|
||||||
val sliderR1ValueText = findViewById<TextView>(R.id.sliderR1Value)
|
|
||||||
val sliderR2ValueText = findViewById<TextView>(R.id.sliderR2Value)
|
|
||||||
val sliderR3ValueText = findViewById<TextView>(R.id.sliderR3Value)
|
|
||||||
|
|
||||||
val sliderR1 = findViewById<SeekBar>(R.id.sliderR1)
|
|
||||||
val sliderR2 = findViewById<SeekBar>(R.id.sliderR2)
|
|
||||||
val sliderR3 = findViewById<SeekBar>(R.id.sliderR3)
|
|
||||||
|
|
||||||
sliderR1.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
||||||
sliderR1Value = progress
|
|
||||||
sliderR1ValueText.text = getString(R.string.r_slider_text, 1, progress)
|
|
||||||
}
|
|
||||||
override fun onStartTrackingTouch(p0: SeekBar?) {}
|
|
||||||
override fun onStopTrackingTouch(p0: SeekBar?) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
sliderR2.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
||||||
sliderR2Value = progress
|
|
||||||
sliderR2ValueText.text = getString(R.string.r_slider_text, 2, progress)
|
|
||||||
}
|
|
||||||
override fun onStartTrackingTouch(p0: SeekBar?) {}
|
|
||||||
override fun onStopTrackingTouch(p0: SeekBar?) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
sliderR3.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
||||||
sliderR3Value = progress
|
|
||||||
sliderR3ValueText.text = getString(R.string.r_slider_text, 3, progress)
|
|
||||||
}
|
|
||||||
override fun onStartTrackingTouch(p0: SeekBar?) {}
|
|
||||||
override fun onStopTrackingTouch(p0: SeekBar?) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package com.listerk.helible
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.bluetooth.le.BluetoothLeScanner
|
|
||||||
import android.bluetooth.le.ScanCallback
|
|
||||||
import android.bluetooth.le.ScanResult
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
//import android.os.Handler
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.ImageView
|
|
||||||
//import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
|
||||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
||||||
bluetoothManager.adapter
|
|
||||||
}
|
|
||||||
private val bleScanner: BluetoothLeScanner by lazy {
|
|
||||||
bluetoothAdapter.bluetoothLeScanner
|
|
||||||
}
|
|
||||||
private val requiredPermissions: MutableSet<String> = mutableSetOf(Manifest.permission.BLUETOOTH_SCAN)
|
|
||||||
private val foundDevices = mutableListOf<BleDevice>()
|
|
||||||
|
|
||||||
private val leScanCallback: ScanCallback = object : ScanCallback() {
|
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
||||||
super.onScanResult(callbackType, result)
|
|
||||||
val bleDevicesView = findViewById<RecyclerView>(R.id.bleDevicesView)
|
|
||||||
val loader = findViewById<ImageView>(R.id.bleDevicesLoader)
|
|
||||||
|
|
||||||
if (result.device.name.isNullOrBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loader.visibility == View.VISIBLE) {
|
|
||||||
(loader.drawable as? Animatable)?.stop()
|
|
||||||
loader.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
var deviceIndex: Int = foundDevices.indexOfFirst { it.address == result.device.address }
|
|
||||||
if (deviceIndex < 0) {
|
|
||||||
deviceIndex = foundDevices.size
|
|
||||||
foundDevices.add(BleDevice(result.device.name, result.device.address, result.rssi))
|
|
||||||
} else {
|
|
||||||
foundDevices[deviceIndex].rssi = result.rssi
|
|
||||||
}
|
|
||||||
bleDevicesView.adapter?.notifyItemChanged(deviceIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
val gotoControlButton = findViewById<Button>(R.id.gotoControlButton)
|
|
||||||
gotoControlButton.setOnClickListener {
|
|
||||||
val intent = Intent(this@MainActivity, ControlActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
val loader = findViewById<ImageView>(R.id.bleDevicesLoader)
|
|
||||||
(loader.drawable as? Animatable)?.start()
|
|
||||||
|
|
||||||
val bleDevicesView = findViewById<RecyclerView>(R.id.bleDevicesView)
|
|
||||||
bleDevicesView.layoutManager = LinearLayoutManager(this)
|
|
||||||
bleDevicesView.adapter = BleDeviceAdapter(foundDevices, gotoControlButton)
|
|
||||||
bleDevicesView.itemAnimator = null
|
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
|
|
||||||
{
|
|
||||||
Log.e("StartActivity", "We have denied permissions")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
||||||
{
|
|
||||||
ActivityCompat.requestPermissions(this@MainActivity, arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*if(Build.VERSION.SDK_INT < 31) {
|
|
||||||
requiredPermissions.add(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
requiredPermissions.add(android.Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
||||||
requiredPermissions.add(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
||||||
}
|
|
||||||
requestPermissions()*/
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
/*if (!hasNecessaryPermissions()) {
|
|
||||||
requestPermissions()
|
|
||||||
} else {*/
|
|
||||||
bleScanner.startScan(leScanCallback)
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
/*if (!hasNecessaryPermissions()) {
|
|
||||||
requestPermissions()
|
|
||||||
} else {*/
|
|
||||||
bleScanner.stopScan(leScanCallback)
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasNecessaryPermissions(): Boolean
|
|
||||||
{
|
|
||||||
for (it in requiredPermissions) {
|
|
||||||
if (ActivityCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
Log.i("ScanActivity", "Permission isn't allowed: $it")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
private fun requestPermissions()
|
|
||||||
{
|
|
||||||
Log.i("ScanActivity", "Trying to request permissions: $requiredPermissions")
|
|
||||||
ActivityCompat.requestPermissions(this@MainActivity, requiredPermissions.toTypedArray(), 1)
|
|
||||||
if(!hasNecessaryPermissions())
|
|
||||||
{
|
|
||||||
Log.e("ScanActivity", "Failed to get all permissions")
|
|
||||||
} else {
|
|
||||||
Log.i("ScanActivity", "App got permissions: $requiredPermissions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true">
|
|
||||||
<shape>
|
|
||||||
<solid android:color="?attr/selectedBlockColor" />
|
|
||||||
<corners android:radius="20dp"/>
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item android:state_selected="false">
|
|
||||||
<shape>
|
|
||||||
<solid android:color="?attr/baseBlockColor" />
|
|
||||||
<corners android:radius="20dp"/>
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M336,680L480,536L624,680L680,624L536,480L680,336L624,280L480,424L336,280L280,336L424,480L280,624L336,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M384,624L440,567L353,480L440,393L384,336L240,480L384,624ZM576,624L720,480L576,336L520,393L607,480L520,567L576,624ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M756,840L537,621L621,537L840,756L756,840ZM204,840L120,756L396,480L328,412L300,440L249,389L249,471L221,499L100,378L128,350L210,350L160,300L302,158Q322,138 345,129Q368,120 392,120Q416,120 439,129Q462,138 482,158L390,250L440,300L412,328L480,396L570,306Q566,295 563.5,283Q561,271 561,259Q561,200 601.5,159.5Q642,119 701,119Q716,119 729.5,122Q743,125 757,131L658,230L730,302L829,203Q836,217 838.5,230.5Q841,244 841,259Q841,318 800.5,358.5Q760,399 701,399Q689,399 677,397Q665,395 654,390L204,840Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,720Q580,720 650,650Q720,580 720,480Q720,380 650,310Q580,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720ZM480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,546 593,593Q546,640 480,640ZM452,508Q463,519 480,519Q497,519 508,508L564,452Q575,441 575,424Q575,407 564,396Q553,385 536,385Q519,385 508,396L452,452Q441,463 441,480Q441,497 452,508ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M360,880L360,840Q360,808 384,786.5Q408,765 440,760L440,760L440,474Q418,466 401.5,451Q385,436 374,416L298,436Q262,444 229.5,430Q197,416 188,382L362,338Q370,297 400,270Q430,243 472,240L496,150Q506,115 534.5,94.5Q563,74 598,82L548,262Q573,278 586.5,304Q600,330 600,360Q600,373 597,385.5Q594,398 590,410L644,466Q669,492 673.5,527Q678,562 654,586L534,466Q531,469 527.5,470.5Q524,472 520,474L520,760L520,760Q552,765 576,786.5Q600,808 600,840L600,880L360,880ZM480,420Q505,420 522.5,402.5Q540,385 540,360Q540,335 522.5,317.5Q505,300 480,300Q455,300 437.5,317.5Q420,335 420,360Q420,385 437.5,402.5Q455,420 480,420Z"/>
|
||||||
|
</vector>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M360,520L360,280L360,280Q260,280 190,350Q120,420 120,520L120,520L360,520ZM520,680L520,280L440,280L440,600L120,600L120,680Q120,680 120,680Q120,680 120,680L520,680ZM600,552L840,528L840,480L600,480L600,552ZM520,880L120,880L120,800L520,800L520,880ZM600,760L120,760Q87,760 63.5,736.5Q40,713 40,680L40,520Q40,386 133,293Q226,200 360,200L600,200L600,400L800,400L840,320L920,320L920,600L600,632L600,760ZM760,160L120,160L120,80L760,80L760,160ZM600,552L600,480L600,480L600,552L600,552ZM520,680L520,680Q520,680 520,680Q520,680 520,680L520,680L520,680L520,680L520,680L520,680Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M200,800L200,520L120,520L120,440L360,440L360,520L280,520L280,800L200,800ZM200,360L200,160L280,160L280,360L200,360ZM360,360L360,280L440,280L440,160L520,160L520,280L600,280L600,360L360,360ZM440,800L440,440L520,440L520,800L440,800ZM680,800L680,680L600,680L600,600L840,600L840,680L760,680L760,800L680,800ZM680,520L680,160L760,160L760,520L680,520Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/holo_orange_light"
|
||||||
|
android:pathData="M272,520L480,640Q480,640 480,640Q480,640 480,640L688,520L520,423L520,560L440,560L440,423L272,520ZM440,331L440,314Q396,301 368,264.5Q340,228 340,180Q340,122 381,81Q422,40 480,40Q538,40 579,81Q620,122 620,180Q620,228 592,264.5Q564,301 520,314L520,331L800,492Q819,503 829.5,521.5Q840,540 840,562L840,638Q840,660 829.5,678.5Q819,697 800,708L520,869Q501,880 480,880Q459,880 440,869L160,708Q141,697 130.5,678.5Q120,660 120,638L120,562Q120,540 130.5,521.5Q141,503 160,492L440,331ZM440,709L200,571L200,638Q200,638 200,638Q200,638 200,638L480,800Q480,800 480,800Q480,800 480,800L760,638Q760,638 760,638Q760,638 760,638L760,571L520,709Q501,720 480,720Q459,720 440,709ZM480,240Q505,240 522.5,222.5Q540,205 540,180Q540,155 522.5,137.5Q505,120 480,120Q455,120 437.5,137.5Q420,155 420,180Q420,205 437.5,222.5Q455,240 480,240ZM480,800Q480,800 480,800Q480,800 480,800L480,800L480,800Q480,800 480,800Q480,800 480,800L480,800Q480,800 480,800Q480,800 480,800L480,800Q480,800 480,800Q480,800 480,800L480,800L480,800Z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<animated-vector
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt">
|
|
||||||
<aapt:attr name="android:drawable">
|
|
||||||
<vector
|
|
||||||
android:name="vector"
|
|
||||||
android:width="60dp"
|
|
||||||
android:height="15dp"
|
|
||||||
android:viewportWidth="40"
|
|
||||||
android:viewportHeight="10">
|
|
||||||
<path
|
|
||||||
android:name="path"
|
|
||||||
android:pathData="M 5 0 C 3.674 0 2.402 0.527 1.464 1.464 C 0.527 2.402 0 3.674 0 5 C 0 6.326 0.527 7.598 1.464 8.536 C 2.402 9.473 3.674 10 5 10 C 6.326 10 7.598 9.473 8.536 8.536 C 9.473 7.598 10 6.326 10 5 C 10 3.674 9.473 2.402 8.536 1.464 C 7.598 0.527 6.326 0 5 0 Z"
|
|
||||||
android:fillColor="?android:textColorPrimary"
|
|
||||||
android:fillAlpha="0"
|
|
||||||
android:strokeWidth="1"/>
|
|
||||||
<path
|
|
||||||
android:name="path_1"
|
|
||||||
android:pathData="M 20 0 C 18.674 0 17.402 0.527 16.464 1.464 C 15.527 2.402 15 3.674 15 5 C 15 6.326 15.527 7.598 16.464 8.536 C 17.402 9.473 18.674 10 20 10 C 21.326 10 22.598 9.473 23.536 8.536 C 24.473 7.598 25 6.326 25 5 C 25 3.674 24.473 2.402 23.536 1.464 C 22.598 0.527 21.326 0 20 0 Z"
|
|
||||||
android:fillColor="?android:textColorPrimary"
|
|
||||||
android:fillAlpha="0"
|
|
||||||
android:strokeWidth="1"/>
|
|
||||||
<path
|
|
||||||
android:name="path_2"
|
|
||||||
android:pathData="M 35 0 C 33.674 0 32.402 0.527 31.464 1.464 C 30.527 2.402 30 3.674 30 5 C 30 6.326 30.527 7.598 31.464 8.536 C 32.402 9.473 33.674 10 35 10 C 36.326 10 37.598 9.473 38.536 8.536 C 39.473 7.598 40 6.326 40 5 C 40 3.674 39.473 2.402 38.536 1.464 C 37.598 0.527 36.326 0 35 0 Z"
|
|
||||||
android:fillColor="?android:textColorPrimary"
|
|
||||||
android:fillAlpha="0"
|
|
||||||
android:strokeWidth="1"/>
|
|
||||||
</vector>
|
|
||||||
</aapt:attr>
|
|
||||||
<target android:name="path">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:duration="500"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
android:valueType="floatType"
|
|
||||||
android:repeatCount="infinite"
|
|
||||||
android:repeatMode="reverse"
|
|
||||||
android:interpolator="@android:anim/linear_interpolator"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
<target android:name="path_1">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:startOffset="200"
|
|
||||||
android:duration="500"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
android:valueType="floatType"
|
|
||||||
android:repeatCount="infinite"
|
|
||||||
android:repeatMode="reverse"
|
|
||||||
android:interpolator="@android:anim/linear_interpolator"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
<target android:name="path_2">
|
|
||||||
<aapt:attr name="android:animation">
|
|
||||||
<objectAnimator
|
|
||||||
android:propertyName="fillAlpha"
|
|
||||||
android:startOffset="400"
|
|
||||||
android:duration="500"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
android:valueType="floatType"
|
|
||||||
android:repeatCount="infinite"
|
|
||||||
android:repeatMode="reverse"
|
|
||||||
android:interpolator="@android:anim/linear_interpolator"/>
|
|
||||||
</aapt:attr>
|
|
||||||
</target>
|
|
||||||
</animated-vector>
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L480,120L480,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L480,760L480,840L200,840ZM640,680L585,622L687,520L360,520L360,440L687,440L585,338L640,280L840,480L640,680Z"/>
|
||||||
|
</vector>
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
android:fillColor="?android:colorPrimary"
|
android:fillColor="?android:colorPrimary"
|
||||||
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
|
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorButtonNormal"
|
android:fillColor="?android:colorControlHighlight"
|
||||||
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
|
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorButtonNormal"
|
android:fillColor="?android:colorControlHighlight"
|
||||||
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
|
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorButtonNormal"
|
android:fillColor="?android:colorControlHighlight"
|
||||||
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
|
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
|
||||||
</vector>
|
</vector>
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
android:fillColor="?android:colorPrimary"
|
android:fillColor="?android:colorPrimary"
|
||||||
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
|
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorButtonNormal"
|
android:fillColor="?android:colorControlHighlight"
|
||||||
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
|
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorButtonNormal"
|
android:fillColor="?android:colorControlHighlight"
|
||||||
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
|
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
|
||||||
</vector>
|
</vector>
|
||||||
@@ -15,6 +15,6 @@
|
|||||||
android:fillColor="?android:colorPrimary"
|
android:fillColor="?android:colorPrimary"
|
||||||
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
|
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorButtonNormal"
|
android:fillColor="?android:colorControlHighlight"
|
||||||
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
|
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
|
||||||
</vector>
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M160,800L160,720L270,720L254,706Q202,660 181,601Q160,542 160,482Q160,371 226.5,284.5Q293,198 400,170L400,254Q328,280 284,342.5Q240,405 240,482Q240,527 257,569.5Q274,612 310,648L320,658L320,560L400,560L400,800L160,800ZM560,790L560,706Q632,680 676,617.5Q720,555 720,478Q720,433 703,390.5Q686,348 650,312L640,302L640,400L560,400L560,160L800,160L800,240L690,240L706,254Q755,303 777.5,360.5Q800,418 800,478Q800,589 733.5,675.5Q667,762 560,790Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?android:colorPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M440,840L440,600L520,600L520,680L840,680L840,760L520,760L520,840L440,840ZM120,760L120,680L360,680L360,760L120,760ZM280,600L280,520L120,520L120,440L280,440L280,360L360,360L360,600L280,600ZM440,520L440,440L840,440L840,520L440,520ZM600,360L600,120L680,120L680,200L840,200L840,280L680,280L680,360L600,360ZM120,280L120,200L520,200L520,280L120,280Z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/controlDeviceName"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Устройство не задано"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<SeekBar
|
|
||||||
android:id="@+id/sliderR1"
|
|
||||||
android:layout_width="300dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:max="65535"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/controlDeviceName"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/sliderR1Value"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="R1: 0 / 65536"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/sliderR1"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<SeekBar
|
|
||||||
android:id="@+id/sliderR2"
|
|
||||||
android:layout_width="300dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:max="65535"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/sliderR1Value"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/sliderR2Value"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="R2: 0 / 65536"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/sliderR2"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<SeekBar
|
|
||||||
android:id="@+id/sliderR3"
|
|
||||||
android:layout_width="300dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:max="65535"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/sliderR2Value"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/sliderR3Value"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="R3: 0 / 65536"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/sliderR3"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ControlActivity">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/devicesTitle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:textSize="24sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="Устройства" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/bleDevicesView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/devicesTitle"
|
|
||||||
tools:listitem="@layout/item_ble_device" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/bleDevicesLoader"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/loader"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/bleDevicesView" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/gotoControlButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Далее"
|
|
||||||
android:enabled="false"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:layout_marginBottom="12dp"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="12dp"
|
|
||||||
android:layout_marginVertical="4dp"
|
|
||||||
android:paddingHorizontal="12dp"
|
|
||||||
android:paddingVertical="8dp"
|
|
||||||
android:background="@drawable/ble_device_bg">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/deviceName"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="Helicopter BLE" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/deviceAddress"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/deviceName"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:text="C3:7A:F4:6E:2F:D6" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/deviceRssi"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:src="@drawable/signal_icon4" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Base.Theme.HeliBLE" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<item name="baseBlockColor">@color/material_dynamic_neutral30</item>
|
|
||||||
<item name="selectedBlockColor">@color/material_dynamic_neutral20</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<attr name="baseBlockColor" type="reference" />
|
|
||||||
<attr name="selectedBlockColor" type="reference" />
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">HeliBLE</string>
|
<string name="app_name">Digital Pilot</string>
|
||||||
<string name="r_slider_text">R%1$d: %2$d / 65536</string>
|
|
||||||
<string name="control_device_text">Устройство: %1$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Base application theme. -->
|
<resources>
|
||||||
<style name="Base.Theme.HeliBLE" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<item name="baseBlockColor">@color/material_dynamic_neutral90</item>
|
|
||||||
<item name="selectedBlockColor">@color/material_dynamic_neutral80</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.HeliBLE" parent="Base.Theme.HeliBLE" />
|
<style name="Theme.Testblue" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
</resources>
|
</resources>
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.listerk.helible
|
package com.helible.pilot
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
plugins {
|
|
||||||
id 'com.android.application' version '8.1.0' apply false
|
|
||||||
id 'com.android.library' version '8.1.0' apply false
|
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.1.0" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
||||||
|
id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#Fri May 05 21:22:26 NOVT 2023
|
#Sun Aug 13 15:00:54 KRAT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ dependencyResolutionManagement {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "HeliBLE"
|
|
||||||
include ':app'
|
rootProject.name = "pilot"
|
||||||
|
include(":app")
|
||||||
Reference in New Issue
Block a user