Compare commits
20
Commits
main
..
027116e18e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
027116e18e | ||
|
|
c8abfd94c3 | ||
|
|
0763c2e1df | ||
|
|
3ac39136c6 | ||
|
|
5e0f2f1bb7 | ||
|
|
3517414ec1 | ||
|
|
3b62743481 | ||
|
|
18bd21fba1 | ||
|
|
77a3b19b24 | ||
|
|
70cd547fb7 | ||
|
|
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
|
||||
.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
|
||||
|
||||
# Signing files
|
||||
.signing/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
|
||||
# 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
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
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
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
|
||||
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
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</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"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.20" />
|
||||
<option name="version" value="1.8.10" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</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,75 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
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")
|
||||
implementation("com.github.manalkaff:JetStick:1.2")
|
||||
|
||||
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",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.listerk.helible",
|
||||
"variantName": "debug",
|
||||
"applicationId": "com.helible.pilot",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
@@ -13,7 +13,7 @@
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "app-debug.apk"
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
+4
-6
@@ -1,13 +1,11 @@
|
||||
package com.listerk.helible
|
||||
package com.helible.pilot
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@@ -19,6 +17,6 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.listerk.helible", appContext.packageName)
|
||||
assertEquals("com.gogacoder.testblue", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,35 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Legacy permissions (API 30 and below) -->
|
||||
<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.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_BACKGROUND_LOCATION" android:maxSdkVersion="30"/>-->
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<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.ACCESS_FINE_LOCATION"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_COARSE_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-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" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -24,16 +40,14 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.HeliBLE">
|
||||
android:theme="@style/Theme.Main"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ControlActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:name="com.helible.pilot.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Main">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,219 @@
|
||||
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.CalibrationPage
|
||||
import com.helible.pilot.components.NotImplementedPage
|
||||
import com.helible.pilot.components.RotorsTestPage
|
||||
import com.helible.pilot.components.console.ConsolePage
|
||||
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
|
||||
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
||||
import com.helible.pilot.components.pidSettings.PidSettingsPage
|
||||
import com.helible.pilot.components.scannerScreen.ScannerScreen
|
||||
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.AppPreferences
|
||||
import com.helible.pilot.viewmodels.BluetoothViewModel
|
||||
import com.helible.pilot.viewmodels.BluetoothViewModelFactory
|
||||
import com.helible.pilot.viewmodels.PermissionDialogViewModel
|
||||
import com.helible.pilot.viewmodels.PreferencesViewModel
|
||||
import com.helible.pilot.viewmodels.SavedPreferencesImpl
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
// TODO: move text strings to resources
|
||||
|
||||
private val preferences by lazy {
|
||||
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
||||
}
|
||||
private val preferencesViewModel by lazy {
|
||||
PreferencesViewModel(preferences)
|
||||
}
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
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()
|
||||
val rotorsDuty by bluetoothViewModel.rotorsDuty.collectAsState()
|
||||
|
||||
LaunchedEffect(key1 = null) {
|
||||
permissionLauncher.launch()
|
||||
}
|
||||
|
||||
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") {
|
||||
ScannerScreen(
|
||||
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 = {
|
||||
preferencesViewModel.clearPreferences()
|
||||
bluetoothViewModel.disconnectFromDevice()
|
||||
},
|
||||
deviceActionsList = defaultDeviceActionsList()
|
||||
)
|
||||
if (preferencesViewModel.preferences != null) BackHandler {}
|
||||
}
|
||||
composable("console/{title}")
|
||||
{ backStackEntry ->
|
||||
ConsolePage(
|
||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||
navigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable("codeblocks/{title}")
|
||||
{ backStackEntry ->
|
||||
NotImplementedPage(
|
||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||
navigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable("imu_calibration/{title}")
|
||||
{ backStackEntry ->
|
||||
CalibrationPage(
|
||||
deviceStatus = bluetoothState.deviceState?.status,
|
||||
startCalibration = { bluetoothViewModel.startImuCalibration() },
|
||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||
navigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable("motor_test/{title}")
|
||||
{ backStackEntry ->
|
||||
RotorsTestPage(
|
||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||
rotorsDuty = rotorsDuty,
|
||||
setRotorsDuty = { bluetoothViewModel.setRotorsDuty(it) },
|
||||
startTelemetrySending = { bluetoothViewModel.startRotorsConfigurationTelemetry() },
|
||||
stopRotors = { bluetoothViewModel.stopRotors() },
|
||||
navigateBack = {
|
||||
navController.popBackStack()
|
||||
bluetoothViewModel.stopRotorsConfigurationTelemetry()
|
||||
bluetoothViewModel.stopRotors()
|
||||
}
|
||||
)
|
||||
BackHandler {
|
||||
navController.popBackStack()
|
||||
bluetoothViewModel.stopRotorsConfigurationTelemetry()
|
||||
bluetoothViewModel.stopRotors()
|
||||
}
|
||||
}
|
||||
composable("pid_settings/{title}")
|
||||
{ backStackEntry ->
|
||||
PidSettingsPage(
|
||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||
navigateBack = {
|
||||
navController.popBackStack()
|
||||
bluetoothViewModel.clearPidSettings()
|
||||
},
|
||||
requestPidSettings = { bluetoothViewModel.requestPidSettings() },
|
||||
setPidSettings = {settings -> bluetoothViewModel.applyPidSettings(settings)},
|
||||
deviceState = bluetoothState.deviceState
|
||||
)
|
||||
BackHandler {
|
||||
navController.popBackStack()
|
||||
bluetoothViewModel.clearPidSettings()
|
||||
}
|
||||
}
|
||||
composable("reports/{title}")
|
||||
{ backStackEntry ->
|
||||
NotImplementedPage(
|
||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||
navigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.helible.pilot.components
|
||||
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun BlankPage(title: String, navigateBack: () -> Unit, block: @Composable () -> Unit) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
IconButton(onClick = { navigateBack() }) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BlankPagePreview() {
|
||||
Surface {
|
||||
BlankPage(title = "Blank page", navigateBack = {}) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp)) {
|
||||
Text(text = "This page in development")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.helible.pilot.components
|
||||
|
||||
import android.widget.Spinner
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.dataclasses.ChangedDeviceStatus
|
||||
import com.helible.pilot.dataclasses.DeviceState
|
||||
import com.helible.pilot.dataclasses.DeviceStatus
|
||||
|
||||
@Composable
|
||||
fun CalibrationPage(
|
||||
deviceStatus: DeviceStatus?,
|
||||
title: String,
|
||||
startCalibration: () -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
) {
|
||||
BlankPage(title = title, navigateBack = navigateBack) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = LocalContext.current.getString(R.string.calibration_description),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
)
|
||||
Button(
|
||||
enabled = deviceStatus != DeviceStatus.IsImuCalibration,
|
||||
onClick = startCalibration,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
if(deviceStatus != DeviceStatus.IsImuCalibration) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.tune),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(3.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Начать калибровку"
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator ()
|
||||
Text(
|
||||
text = "Калибровка...",
|
||||
modifier = Modifier.padding(5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CalibrationPagePreview() {
|
||||
Surface {
|
||||
CalibrationPage(
|
||||
DeviceStatus.IsImuCalibration,
|
||||
title = "Калибровка гироскопа и акселерометра",
|
||||
startCalibration = {},
|
||||
navigateBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RequiredHardwareFeaturesPreview() {
|
||||
RequiredHardwareFeatures(
|
||||
title = "Turn on Bluetooth",
|
||||
description = "App requires Bluetooth turned on to continue",
|
||||
confirmButtonText = "Turn on",
|
||||
featureState = false,
|
||||
requestFeature = {},
|
||||
onDismissRequest = {}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.helible.pilot.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.components.BlankPage
|
||||
|
||||
@Composable
|
||||
fun NotImplementedPage(title: String, navigateBack: () -> Unit) {
|
||||
BlankPage(title = title, navigateBack = navigateBack) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)) {
|
||||
Text(text = "Эта страница пока не готова и находится на стадии разработки")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
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.tooling.preview.Preview
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PermissionDialogPreview() {
|
||||
PermissionDialog(
|
||||
LocationPermissionTextProvider(),
|
||||
false,
|
||||
{}, {}, {}, {}
|
||||
)
|
||||
}
|
||||
|
||||
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,118 @@
|
||||
package com.helible.pilot.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.dataclasses.RotorsDuty
|
||||
import com.helible.pilot.ui.theme.TestblueTheme
|
||||
|
||||
@Composable
|
||||
fun RotorsTestPage(
|
||||
title: String,
|
||||
rotorsDuty: RotorsDuty,
|
||||
setRotorsDuty: (duty: RotorsDuty) -> Unit,
|
||||
startTelemetrySending: () -> Unit,
|
||||
stopRotors: () -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
BlankPage(title = title, navigateBack = navigateBack) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
LaunchedEffect(null) {
|
||||
startTelemetrySending()
|
||||
}
|
||||
Text(
|
||||
text = "R1",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
Slider(
|
||||
value = rotorsDuty.r1.toFloat(),
|
||||
onValueChange = { setRotorsDuty(rotorsDuty.copy(r1 = it.toInt().toShort())) },
|
||||
valueRange = 0f..5000f,
|
||||
steps = 10
|
||||
)
|
||||
Text(
|
||||
text = "При перемещении слайдера вправо ротор 1 должен вращаться против часовой стрелки, если смотреть сверху.",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "R2",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
Slider(
|
||||
value = rotorsDuty.r2.toFloat(),
|
||||
onValueChange = { setRotorsDuty(rotorsDuty.copy(r2 = it.toInt().toShort())) },
|
||||
valueRange = -5000f..5000f,
|
||||
steps = 10
|
||||
)
|
||||
Text(
|
||||
text = "При перемещении слайдера вправо ротор 1 должен вращаться по часовой стрелке, если смотреть сверху.",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "R3",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
)
|
||||
Slider(
|
||||
value = rotorsDuty.r3.toFloat(),
|
||||
onValueChange = { setRotorsDuty(rotorsDuty.copy(r3 = it.toInt().toShort())) },
|
||||
valueRange = 0f..5000f,
|
||||
steps = 10
|
||||
)
|
||||
Text(
|
||||
text = "При отклонении слайдера вправо от центра ротор 1 должен вращаться по часовой стрелке, а при отклонении влево - против часовой, если смотреть сверху.",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
FloatingActionButton(
|
||||
onClick = { stopRotors() },
|
||||
containerColor = Color(245, 47, 7),
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "СТОП",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun RotorsTestPagePreview() {
|
||||
TestblueTheme {
|
||||
RotorsTestPage(
|
||||
title = "Тестирование моторов",
|
||||
rotorsDuty = RotorsDuty(5, 5, 5),
|
||||
setRotorsDuty = { _ -> },
|
||||
startTelemetrySending = {},
|
||||
stopRotors = {},
|
||||
navigateBack = { /*TODO*/ }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun Title(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontSize: TextUnit = 23.sp,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier,
|
||||
fontSize = fontSize,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.helible.pilot.components.console
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.helible.pilot.components.BlankPage
|
||||
import com.helible.pilot.dataclasses.DeviceStatus
|
||||
import com.manalkaff.jetstick.JoyStick
|
||||
|
||||
@Composable
|
||||
fun ConsolePage(
|
||||
title: String,
|
||||
navigateBack: () -> Unit
|
||||
) {
|
||||
BlankPage(title = title, navigateBack = navigateBack) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
JoyStick(
|
||||
Modifier.padding(30.dp),
|
||||
size = 150.dp,
|
||||
dotSize = 30.dp
|
||||
){ x: Float, y: Float ->
|
||||
Log.d("JoyStick", "$x, $y")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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.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.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.dataclasses.DeviceStatus
|
||||
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)
|
||||
if(bluetoothUiState.isConnected) {
|
||||
val deviceStatus = bluetoothUiState.deviceState?.status
|
||||
if (deviceStatus != null) {
|
||||
Text(text = "Заряд батареи: ${bluetoothUiState.deviceState.batteryCharge}%")
|
||||
if (deviceStatus == DeviceStatus.ChargeRequired) {
|
||||
Text(text = "Аккумулятор разряжен", color = Color.Red)
|
||||
} else {
|
||||
Text(text = deviceStatus.description())
|
||||
}
|
||||
} else {
|
||||
Text(text = "Ожиданием рукопожатия...")
|
||||
}
|
||||
}
|
||||
}
|
||||
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("Подключение...")
|
||||
} else {
|
||||
Text("Попытка подключения не удалась.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceConnectionStatusPreview() {
|
||||
Surface {
|
||||
DeviceConnectionStatus(bluetoothState = BluetoothUiState(isConnecting = true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.helible.pilot.components.deviceScreen
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.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 = 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 = { navigateToPage(action.first + '/' + action.second.second) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
"Отчеты о полётах"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package com.helible.pilot.components.pidSettings
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import com.helible.pilot.R
|
||||
import com.helible.pilot.components.BlankPage
|
||||
import com.helible.pilot.dataclasses.DeviceState
|
||||
import com.helible.pilot.dataclasses.DeviceStatus
|
||||
import com.helible.pilot.dataclasses.PidParams
|
||||
import com.helible.pilot.dataclasses.PidSettings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PidSettingsPage(
|
||||
title: String,
|
||||
navigateBack: () -> Unit,
|
||||
requestPidSettings: () -> Unit,
|
||||
setPidSettings: (PidSettings) -> Unit,
|
||||
deviceState: DeviceState?,
|
||||
) {
|
||||
BlankPage(title = title, navigateBack = navigateBack) {
|
||||
var pValue by remember { mutableStateOf("") }
|
||||
var iValue by remember { mutableStateOf("") }
|
||||
var dValue by remember { mutableStateOf("") }
|
||||
var selectedRegulator by remember { mutableStateOf("") }
|
||||
val dropdownMenuItems =
|
||||
listOf("Контроллер высоты", "Контроллер крена", "Контроллер рысканья")
|
||||
|
||||
LaunchedEffect(null) {
|
||||
requestPidSettings()
|
||||
}
|
||||
LaunchedEffect(deviceState?.pidSettings) {
|
||||
if (deviceState?.pidSettings != null) {
|
||||
val pidSettings = deviceState.pidSettings
|
||||
when (selectedRegulator) {
|
||||
dropdownMenuItems[0] -> {
|
||||
pidSettings.heightControllerParams.p.toString().also { pValue = it }
|
||||
pidSettings.heightControllerParams.i.toString().also { iValue = it }
|
||||
pidSettings.heightControllerParams.d.toString().also { dValue = it }
|
||||
}
|
||||
|
||||
dropdownMenuItems[1] -> {
|
||||
pidSettings.yawControllerParams.p.toString().also { pValue = it }
|
||||
pidSettings.yawControllerParams.i.toString().also { iValue = it }
|
||||
pidSettings.yawControllerParams.d.toString().also { dValue = it }
|
||||
}
|
||||
|
||||
dropdownMenuItems[2] -> {
|
||||
pidSettings.pitchControllerParams.p.toString().also { pValue = it }
|
||||
pidSettings.pitchControllerParams.i.toString().also { iValue = it }
|
||||
pidSettings.pitchControllerParams.d.toString().also { dValue = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
{
|
||||
if (deviceState?.status != DeviceStatus.Idle) {
|
||||
Text(
|
||||
text = "Этот раздел доступен только с подключенным заряженным бездействующим устройством.",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else if (deviceState.pidSettings == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(10.dp))
|
||||
Text(text = "Синхронизация...")
|
||||
} else {
|
||||
val pidSettings = deviceState.pidSettings
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 10.dp).padding(bottom = 10.dp)) {
|
||||
Text(
|
||||
"Рекомендации по настройке ПИД регуляторов",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 10.dp)
|
||||
)
|
||||
Text(
|
||||
text = LocalContext.current.getString(R.string.p_pid_value_description),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = LocalContext.current.getString(R.string.i_pid_value_description),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = LocalContext.current.getString(R.string.d_pid_value_description),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedDropdownMenu(
|
||||
label = "ПИД регулятор",
|
||||
suggestions = dropdownMenuItems,
|
||||
onChange = { selected ->
|
||||
Log.i("BluetoothVM", selected)
|
||||
when (dropdownMenuItems.indexOf(selected)) {
|
||||
0 -> {
|
||||
selectedRegulator = dropdownMenuItems[0]
|
||||
pidSettings.heightControllerParams.p.toString().also { pValue = it }
|
||||
pidSettings.heightControllerParams.i.toString().also { iValue = it }
|
||||
pidSettings.heightControllerParams.d.toString().also { dValue = it }
|
||||
}
|
||||
|
||||
1 -> {
|
||||
selectedRegulator = dropdownMenuItems[1]
|
||||
pidSettings.yawControllerParams.p.toString().also { pValue = it }
|
||||
pidSettings.yawControllerParams.i.toString().also { iValue = it }
|
||||
pidSettings.yawControllerParams.d.toString().also { dValue = it }
|
||||
}
|
||||
|
||||
2 -> {
|
||||
selectedRegulator = dropdownMenuItems[2]
|
||||
pidSettings.pitchControllerParams.p.toString().also { pValue = it }
|
||||
pidSettings.pitchControllerParams.i.toString().also { iValue = it }
|
||||
pidSettings.pitchControllerParams.d.toString().also { dValue = it }
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = pValue,
|
||||
onValueChange = { pValue = it },
|
||||
label = { Text(text = "P") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = iValue,
|
||||
onValueChange = { iValue = it },
|
||||
label = { Text(text = "I") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = dValue,
|
||||
onValueChange = { dValue = it },
|
||||
label = { Text(text = "D") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
}
|
||||
val p = pValue
|
||||
val i = iValue
|
||||
val d = dValue
|
||||
Button(
|
||||
onClick = {
|
||||
when (selectedRegulator) {
|
||||
dropdownMenuItems[0] -> {
|
||||
val newPidSettings = pidSettings.copy(
|
||||
heightControllerParams = PidParams(
|
||||
p.toFloat(),
|
||||
i.toFloat(),
|
||||
d.toFloat()
|
||||
)
|
||||
)
|
||||
setPidSettings(newPidSettings)
|
||||
}
|
||||
|
||||
dropdownMenuItems[1] -> {
|
||||
val newPidSettings = pidSettings.copy(
|
||||
yawControllerParams = PidParams(
|
||||
p.toFloat(),
|
||||
i.toFloat(),
|
||||
d.toFloat()
|
||||
)
|
||||
)
|
||||
setPidSettings(newPidSettings)
|
||||
}
|
||||
|
||||
dropdownMenuItems[2] -> {
|
||||
val newPidSettings = pidSettings.copy(
|
||||
pitchControllerParams = PidParams(
|
||||
p.toFloat(),
|
||||
i.toFloat(),
|
||||
d.toFloat()
|
||||
)
|
||||
)
|
||||
setPidSettings(newPidSettings)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = isValidValue(p) && isValidValue(i) && isValidValue(d)
|
||||
) {
|
||||
Text(text = "Применить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidValue(k: String): Boolean {
|
||||
return k.toFloatOrNull() != null && k.toFloat() >= 0f && k.toFloat() <= 15f
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PidSettingsPreview() {
|
||||
PidSettingsPage(
|
||||
title = "Настройки ПИД регуляторов",
|
||||
navigateBack = { },
|
||||
requestPidSettings = { },
|
||||
setPidSettings = {},
|
||||
deviceState = DeviceState(
|
||||
status = DeviceStatus.Idle,
|
||||
pidSettings = PidSettings(
|
||||
PidParams(1f, 1f, 1f),
|
||||
PidParams(1f, 1f, 1f),
|
||||
PidParams(1f, 1f, 1f)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DropdownDemo() {
|
||||
OutlinedDropdownMenu(label = "", suggestions = listOf("A", "B"), onChange = {})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OutlinedDropdownMenu(
|
||||
label: String,
|
||||
suggestions: List<String>,
|
||||
onChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedText by remember { mutableStateOf(suggestions.first()) }
|
||||
var textfieldSize by remember { mutableStateOf(Size.Zero) }
|
||||
|
||||
val icon = if (expanded)
|
||||
Icons.Filled.KeyboardArrowUp
|
||||
else
|
||||
Icons.Filled.KeyboardArrowDown
|
||||
|
||||
|
||||
Column(modifier = modifier) {
|
||||
OutlinedTextField(
|
||||
value = selectedText,
|
||||
onValueChange = {
|
||||
selectedText = it
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onGloballyPositioned { coordinates ->
|
||||
// This value is used to assign to the DropDown the same width
|
||||
textfieldSize = coordinates.size.toSize()
|
||||
},
|
||||
label = { Text(label) },
|
||||
trailingIcon = {
|
||||
Icon(icon, "contentDescription",
|
||||
Modifier.clickable { expanded = !expanded })
|
||||
}
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier
|
||||
.width(with(LocalDensity.current) { textfieldSize.width.toDp() })
|
||||
) {
|
||||
suggestions.forEach { label ->
|
||||
DropdownMenuItem(onClick = {
|
||||
selectedText = label
|
||||
expanded = false
|
||||
onChange(selectedText)
|
||||
},
|
||||
text = { Text(label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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.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.R
|
||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||
|
||||
|
||||
@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()
|
||||
.weight(1f, true)
|
||||
) {
|
||||
Text(
|
||||
text = deviceInfo.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
softWrap = true
|
||||
)
|
||||
Text(
|
||||
text = "MAC: ${deviceInfo.macAddress}",
|
||||
fontWeight = FontWeight.Thin
|
||||
)
|
||||
}
|
||||
if (deviceInfo.isScanned) {
|
||||
val icon = getSignalIconForRssiValue(deviceInfo.rssi)
|
||||
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.weight(0.3f)) {
|
||||
Icon(
|
||||
painterResource(id = icon),
|
||||
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)
|
||||
)
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
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.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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
|
||||
@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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DiscoveredDevicesListPreview() {
|
||||
val state = BluetoothUiState(
|
||||
pairedBluetoothDevices = listOf(
|
||||
BluetoothDevice("My car", "AA:BB:CC:DD:FF", -70, false),
|
||||
BluetoothDevice("Speaker", "AA:BB:CC:DD:FF", -20, false),
|
||||
BluetoothDevice("My TV", "AA:BB:CC:DD:FF", 10, false),
|
||||
BluetoothDevice("My phone", "AA:BB:CC:DD:FF", -50, false),
|
||||
BluetoothDevice("Mi Band 6", "AA:BB:CC:DD:FF", -100, false),
|
||||
),
|
||||
scannedBluetoothDevices = listOf(
|
||||
BluetoothDevice("Watch", "AA:BB:CC:DD:FF", -10, true),
|
||||
BluetoothDevice("Mi Cleaner", "AA:BB:CC:DD:FF", -90, true),
|
||||
BluetoothDevice("My fridge", "AA:BB:CC:DD:FF", -100, true),
|
||||
BluetoothDevice("Unknown device", "AA:BB:CC:DD:FF", -130, true)
|
||||
)
|
||||
)
|
||||
Surface {
|
||||
DiscoveredDevicesList(bluetoothState = state, selectedDevice = null, choiceDevice = {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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.tooling.preview.Preview
|
||||
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.BluetoothDevice
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun ScannerScreen(
|
||||
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 = "Далее")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ScannerScreenPreview() {
|
||||
val state = BluetoothUiState(
|
||||
pairedBluetoothDevices = listOf(
|
||||
BluetoothDevice("My car", "AA:BB:CC:DD:FF", -70, false),
|
||||
BluetoothDevice("Speaker", "AA:BB:CC:DD:FF", -20, false),
|
||||
BluetoothDevice("My TV", "AA:BB:CC:DD:FF", 10, false),
|
||||
BluetoothDevice("My phone", "AA:BB:CC:DD:FF", -50, false),
|
||||
BluetoothDevice("Mi Band 6", "AA:BB:CC:DD:FF", -100, false),
|
||||
),
|
||||
scannedBluetoothDevices = listOf(
|
||||
BluetoothDevice("Watch", "AA:BB:CC:DD:FF", -10, true),
|
||||
BluetoothDevice("Mi Cleaner", "AA:BB:CC:DD:FF", -90, true),
|
||||
BluetoothDevice("My fridge", "AA:BB:CC:DD:FF", -100, true),
|
||||
BluetoothDevice("Unknown device", "AA:BB:CC:DD:FF", -130, true)
|
||||
)
|
||||
)
|
||||
Surface {
|
||||
ScannerScreen(
|
||||
state,
|
||||
state.scannedBluetoothDevices[1],
|
||||
{}, {},
|
||||
{ _ -> },
|
||||
{},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
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.viewmodels.BluetoothDataTransferService
|
||||
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
|
||||
import com.helible.pilot.dataclasses.GeneralMessage
|
||||
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: GeneralMessage) : 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()
|
||||
}
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
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 {
|
||||
Log.i("BluetoothController", "Connecting to device...")
|
||||
currentClientSocket =
|
||||
bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord(
|
||||
UUID.fromString(SERVICE_UUID)
|
||||
)
|
||||
currentClientSocket?.let { socket ->
|
||||
try {
|
||||
socket.connect()
|
||||
emit(ConnectionResult.ConnectionEstablished)
|
||||
BluetoothDataTransferService(socket).also {
|
||||
dataTransferService = it
|
||||
emitAll(
|
||||
it.listenForIncomingMessages()
|
||||
.map { deviceState ->
|
||||
ConnectionResult.TransferSucceded(deviceState)
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
socket.close()
|
||||
currentClientSocket = null
|
||||
Log.e("BluetoothController", "I/O exception: ${e.message}")
|
||||
emit(ConnectionResult.Error("Connection was interrupted"))
|
||||
}
|
||||
}
|
||||
}.onCompletion {
|
||||
Log.i("BluetoothController", "Connection closed on flow completion.")
|
||||
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
|
||||
Log.i("BluetoothController", "Connection closed")
|
||||
}
|
||||
|
||||
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,13 @@
|
||||
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(),
|
||||
val deviceState: DeviceState? = null
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
|
||||
data class ChangedDeviceStatus(
|
||||
val status: DeviceStatus
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeviceState(
|
||||
val status: DeviceStatus = DeviceStatus.ChargeRequired,
|
||||
@Json(name = "charge") val batteryCharge: Int = 10,
|
||||
val flightHeight: Float = 0f,
|
||||
@Json(name = "y") val yaw: Float = 0f,
|
||||
@Json(name = "p") val pitch: Float = 0f,
|
||||
@Json(name = "r") val roll: Float = 0f,
|
||||
@Json(name = "zIn") val zInertial: Float = 0f,
|
||||
val pidSettings: PidSettings? = null
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
enum class DeviceStatus {
|
||||
Idle,
|
||||
IsPreparingForTakeoff,
|
||||
IsFlying,
|
||||
IsBoarding,
|
||||
IsImuCalibration,
|
||||
ChargeRequired;
|
||||
|
||||
fun description(): String {
|
||||
return when (this) {
|
||||
Idle -> "Готово к работе"
|
||||
IsPreparingForTakeoff -> "Подготовка к полёту"
|
||||
IsFlying -> "В полёте"
|
||||
IsBoarding -> "Посадка"
|
||||
IsImuCalibration -> "Калибровка..."
|
||||
ChargeRequired -> "Аккумулятор разряжен"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class DeviceStatusJsonAdapter {
|
||||
@FromJson
|
||||
fun fromJson(deviceStatus: String): DeviceStatus {
|
||||
try {
|
||||
val index: UInt = deviceStatus.toUInt()
|
||||
return DeviceStatus.values()[index.toInt()]
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
throw JsonDataException("Impossible conversation from String to DeviceStatus")
|
||||
}
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(deviceStatus: DeviceStatus): String {
|
||||
return DeviceStatus.values().indexOf(deviceStatus).toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
// This dataclass provide message content with its type without any markers
|
||||
data class GeneralMessage(
|
||||
val type: MessageType,
|
||||
val data: String
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
enum class MessageType {
|
||||
UpdateMessage,
|
||||
PidSettings
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PidParams (
|
||||
val p: Float,
|
||||
val i: Float,
|
||||
val d: Float
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PidSettingRequiredMessage (
|
||||
val pidSettingOpened: Boolean = true
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PidSettings (
|
||||
@Json(name = "p1") val heightControllerParams: PidParams,
|
||||
@Json(name = "p2") val yawControllerParams: PidParams,
|
||||
@Json(name = "p3") val pitchControllerParams: PidParams
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RotorsDuty(
|
||||
val r1: Short,
|
||||
val r2: Short,
|
||||
val r3: Short
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
data class StickPosition (
|
||||
val x: Float,
|
||||
val y: Float
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.helible.pilot.dataclasses
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StopMessage(
|
||||
val stop: Boolean = true
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.helible.pilot.exceptions
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
class TransferFailedException : IOException("Reading incoming data failed")
|
||||
@@ -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,41 @@
|
||||
package com.helible.pilot.receivers
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.location.LocationManager
|
||||
|
||||
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,60 @@
|
||||
package com.helible.pilot.viewmodels
|
||||
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.util.Log
|
||||
import com.helible.pilot.dataclasses.GeneralMessage
|
||||
import com.helible.pilot.dataclasses.MessageType
|
||||
import com.helible.pilot.exceptions.TransferFailedException
|
||||
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.BufferedInputStream
|
||||
import java.io.IOException
|
||||
|
||||
const val maxPackageSize = 512 // [bytes]
|
||||
|
||||
class BluetoothDataTransferService(
|
||||
private val socket: BluetoothSocket,
|
||||
) {
|
||||
fun listenForIncomingMessages(): Flow<GeneralMessage> {
|
||||
return flow {
|
||||
if (!socket.isConnected)
|
||||
return@flow
|
||||
|
||||
val buffer = BufferedInputStream(socket.inputStream, maxPackageSize)
|
||||
while (true) {
|
||||
val message: String = try {
|
||||
buffer.bufferedReader().readLine()
|
||||
} catch (e: IOException) {
|
||||
Log.e("BluetoothController", "Failed to receive incoming data")
|
||||
throw TransferFailedException()
|
||||
}
|
||||
try {
|
||||
val messageType =
|
||||
MessageType.values().elementAtOrNull(message.split(";").first().toInt())
|
||||
val messageData = message.split(";").last()
|
||||
if (messageType != null) {
|
||||
emit(GeneralMessage(messageType, messageData))
|
||||
}
|
||||
} catch (e: NoSuchElementException) {
|
||||
Log.e("BluetoothController", "Message type is invalid: $message")
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
suspend fun sendMessage(bytes: ByteArray): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
socket.outputStream.write(bytes)
|
||||
} catch (e: IOException) {
|
||||
Log.e("BluetoothController", "Failed to write message: $e")
|
||||
return@withContext false
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
package com.helible.pilot.viewmodels
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.helible.pilot.controllers.BluetoothController
|
||||
import com.helible.pilot.controllers.ConnectionResult
|
||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.dataclasses.ChangedDeviceStatus
|
||||
import com.helible.pilot.dataclasses.DeviceState
|
||||
import com.helible.pilot.dataclasses.DeviceStatus
|
||||
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter
|
||||
import com.helible.pilot.dataclasses.MessageType
|
||||
import com.helible.pilot.dataclasses.PidSettingRequiredMessage
|
||||
import com.helible.pilot.dataclasses.PidSettings
|
||||
import com.helible.pilot.dataclasses.RotorsDuty
|
||||
import com.helible.pilot.dataclasses.StopMessage
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonEncodingException
|
||||
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.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
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 _rotorsDuty: MutableStateFlow<RotorsDuty> = MutableStateFlow(RotorsDuty(0, 0, 0))
|
||||
private val _isRotorsTelemetryEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
val rotorsDuty: StateFlow<RotorsDuty>
|
||||
get() = _rotorsDuty.asStateFlow()
|
||||
|
||||
private var rotorsTelemetryJob: Job? = null
|
||||
private var deviceConnectionJob: Job? = null
|
||||
|
||||
private val moshi =
|
||||
Moshi.Builder().add(KotlinJsonAdapterFactory()).add(DeviceStatusJsonAdapter()).build()
|
||||
private val statusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java)
|
||||
private val deviceStateMessageAdapter = moshi.adapter(DeviceState::class.java)
|
||||
private val pidSittingsMessageAdapter = moshi.adapter(PidSettings::class.java)
|
||||
private val pidSittingsRequiredMessageAdapter =
|
||||
moshi.adapter(PidSettingRequiredMessage::class.java)
|
||||
private val rotorDutyMessageAdapter = moshi.adapter(RotorsDuty::class.java)
|
||||
private val stopAllRotorsMessageAdapter = moshi.adapter(StopMessage::class.java)
|
||||
|
||||
companion object {
|
||||
const val messageDelimiter = "\n"
|
||||
const val telemetryPauseDuractionMs: Long = 100
|
||||
}
|
||||
|
||||
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 -> {
|
||||
try {
|
||||
when (result.message.type) {
|
||||
MessageType.PidSettings -> {
|
||||
val newPidSettings =
|
||||
pidSittingsMessageAdapter.fromJson(result.message.data)
|
||||
_state.update {
|
||||
it.copy(
|
||||
deviceState = it.deviceState?.copy(pidSettings = newPidSettings)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MessageType.UpdateMessage -> {
|
||||
val newDeviceState =
|
||||
deviceStateMessageAdapter.fromJson(result.message.data)
|
||||
if (newDeviceState != null) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
deviceState = newDeviceState.copy(pidSettings = it.deviceState?.pidSettings)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (e: JsonDataException) {
|
||||
Log.e("BluetoothVM", "Failed to parse message: ${result.message.data}")
|
||||
} catch (e: JsonEncodingException) {
|
||||
Log.e("BluetoothVM", "Failed to decode message: ${result.message.data}")
|
||||
}
|
||||
}
|
||||
|
||||
is ConnectionResult.Error -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isConnected = false,
|
||||
isConnecting = false,
|
||||
errorMessage = result.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.catch { throwable ->
|
||||
Log.e(
|
||||
"BluetoothController",
|
||||
"Error occured while data transfer: ${throwable.message}"
|
||||
)
|
||||
bluetoothController.closeConnection()
|
||||
_state.update {
|
||||
it.copy(
|
||||
isConnected = false,
|
||||
isConnecting = false,
|
||||
deviceState = null
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun connectToDevice(device: String) {
|
||||
if (_state.value.isConnected or _state.value.isConnecting) {
|
||||
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,
|
||||
deviceState = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 startImuCalibration() {
|
||||
viewModelScope.launch {
|
||||
val message = statusMessageAdapter.toJson(
|
||||
ChangedDeviceStatus(DeviceStatus.IsImuCalibration)
|
||||
) + messageDelimiter
|
||||
val isSuccess = bluetoothController.trySendMessage(
|
||||
message.toByteArray()
|
||||
)
|
||||
if (!isSuccess) {
|
||||
Log.e("BluetoothVM", "Failed to start IMU calibration: $message")
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
deviceState = it.deviceState?.copy(status = DeviceStatus.IsImuCalibration)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPidSettings() {
|
||||
viewModelScope.launch {
|
||||
val message =
|
||||
pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + messageDelimiter
|
||||
Log.i("BluetoothVM", "Requested PID settings: $message")
|
||||
val isSuccess = bluetoothController.trySendMessage(
|
||||
message.toByteArray()
|
||||
)
|
||||
if (!isSuccess) {
|
||||
Log.e("BluetoothVM", "Failed to request PID settings: $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyPidSettings(pidSettings: PidSettings) {
|
||||
viewModelScope.launch {
|
||||
val message = pidSittingsMessageAdapter.toJson(pidSettings) + messageDelimiter
|
||||
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||||
if (!isSuccess) {
|
||||
Log.e("BluetoothVM", "Failed to request PID settings: $message")
|
||||
_state.update {
|
||||
it.copy(errorMessage = "Не удалось обновить значения PID")
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(deviceState = it.deviceState?.copy(pidSettings = pidSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPidSettings() {
|
||||
Log.i("BluetoothVM", "PidSettings cleared")
|
||||
_state.update {
|
||||
it.copy(deviceState = it.deviceState?.copy(pidSettings = null))
|
||||
}
|
||||
Log.i("BluetoothVM", "PidSettings: ${_state.value.deviceState?.pidSettings}")
|
||||
}
|
||||
|
||||
private fun sendRotorsDuty() {
|
||||
viewModelScope.launch {
|
||||
val message = rotorDutyMessageAdapter.toJson(
|
||||
_rotorsDuty.value
|
||||
) + messageDelimiter
|
||||
val isSuccess = bluetoothController.trySendMessage(
|
||||
message.toByteArray()
|
||||
)
|
||||
if (!isSuccess) {
|
||||
Log.e("BluetoothVM", "Failed to send rotors telemetry: $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startRotorsConfigurationTelemetry() {
|
||||
Log.i("BluetoothVM", "Start send rotors configuration telemetry...")
|
||||
if(_isRotorsTelemetryEnabled.value) return
|
||||
_isRotorsTelemetryEnabled.update { true }
|
||||
flow {
|
||||
while(_isRotorsTelemetryEnabled.value) {
|
||||
emit(Unit)
|
||||
delay(telemetryPauseDuractionMs)
|
||||
}
|
||||
}.onEach{
|
||||
sendRotorsDuty()
|
||||
Log.d("BluetoothVM", "Sended rotors telemetry")
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun stopRotorsConfigurationTelemetry() {
|
||||
Log.i("BluetoothVM", "Stop send rotors configuration periodically...")
|
||||
rotorsTelemetryJob = null
|
||||
_isRotorsTelemetryEnabled.update { false }
|
||||
}
|
||||
|
||||
fun setRotorsDuty(newRotorsDuty: RotorsDuty) {
|
||||
_rotorsDuty.update { newRotorsDuty }
|
||||
}
|
||||
|
||||
fun stopRotors() {
|
||||
viewModelScope.launch {
|
||||
val message = stopAllRotorsMessageAdapter.toJson(StopMessage()) + messageDelimiter
|
||||
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||||
if (!isSuccess) {
|
||||
Log.e("BluetoothVM", "Failed to stop all rotors: $message")
|
||||
_state.update {
|
||||
it.copy(errorMessage = "Не удалось остановить моторы!")
|
||||
}
|
||||
} else {
|
||||
_rotorsDuty.update { RotorsDuty(0, 0, 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.helible.pilot.viewmodels
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.helible.pilot.controllers.AndroidBluetoothController
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
@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,10 @@
|
||||
<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>
|
||||
@@ -1,15 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="500" android:viewportHeight="500">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="500"
|
||||
android:viewportHeight="500">
|
||||
<path
|
||||
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" />
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
</vector>
|
||||
@@ -12,9 +12,9 @@
|
||||
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" />
|
||||
<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" />
|
||||
<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" />
|
||||
</vector>
|
||||
@@ -15,6 +15,6 @@
|
||||
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" />
|
||||
<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" />
|
||||
</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"?>
|
||||
<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="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,5 +1,7 @@
|
||||
<resources>
|
||||
<string name="app_name">HeliBLE</string>
|
||||
<string name="r_slider_text">R%1$d: %2$d / 65536</string>
|
||||
<string name="control_device_text">Устройство: %1$s</string>
|
||||
<string name="app_name">Digital Pilot</string>
|
||||
<string name="calibration_description">Расположите устройство на ровной горизонтальной поверхности, чтобы сани вертолета полностью лежали на ней. Нажмите кнопку калибровки ниже и ждите её окончания, не создавая тряски.</string>
|
||||
<string name="p_pid_value_description">Сначала подберите значение коэффицента P, которое балансирует между слишком низкой и слишком высокой чувствительностью.</string>
|
||||
<string name="i_pid_value_description">Затем подберите значение коэффицента I, которое уберёт нежелательный дрейв, но не повлияет на отзывчивость.</string>
|
||||
<string name="d_pid_value_description">После установите значение коэффицента D таким образом, чтобы обеспечить более стабильное и плавное управление.</string>
|
||||
</resources>
|
||||
@@ -1,9 +1,5 @@
|
||||
<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_neutral90</item>
|
||||
<item name="selectedBlockColor">@color/material_dynamic_neutral80</item>
|
||||
</style>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.HeliBLE" parent="Base.Theme.HeliBLE" />
|
||||
<style name="Theme.Main" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
package com.listerk.helible
|
||||
package com.helible.pilot
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user