diff --git a/app/build.gradle b/app/build.gradle
index a2facca..1ae6330 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -50,20 +50,21 @@ android {
dependencies {
- implementation 'androidx.core:core-ktx:1.7.0'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
- implementation 'androidx.activity:activity-compose:1.3.1'
- implementation "androidx.compose.ui:ui:$compose_version"
- implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
+ implementation 'androidx.core:core-ktx:1.9.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
+ implementation 'androidx.activity:activity-compose:1.7.0'
+ implementation "androidx.compose.ui:ui:1.5.0-alpha01"
+ implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-alpha01"
implementation 'androidx.compose.material3:material3:1.1.0-beta01'
+ implementation 'androidx.compose.material:material:1.5.0-alpha01'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
- androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
- debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
- debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0-alpha01"
+ debugImplementation "androidx.compose.ui:ui-tooling:1.5.0-alpha01"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.0-alpha01"
- implementation "androidx.compose.material:material-icons-extended:1.4.0-rc01"
+ implementation "androidx.compose.material:material-icons-extended:1.5.0-alpha01"
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.navigation:navigation-compose:2.5.3'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e3fed92..8d50d78 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,9 @@
+
+
+
+ android:theme="@style/Theme.App.Starting">
diff --git a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt
index 6a979c1..c2969e7 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt
@@ -1,66 +1,162 @@
package llc.arma.ble.app.ui
+import android.Manifest
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BottomState
+import llc.arma.ble.app.ui.common.LocalBottomDialogState
import llc.arma.ble.app.ui.screen.main.MainScreen
import llc.arma.ble.app.ui.theme.BleTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
- @OptIn(ExperimentalPermissionsApi::class)
+ @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
+ installSplashScreen()
+
setContent {
BleTheme {
- Surface(
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding(),
- color = MaterialTheme.colorScheme.background
+ val modalState =
+ rememberModalBottomSheetState(
+ skipHalfExpanded = true,
+ initialValue = ModalBottomSheetValue.Hidden
+ )
+
+ var sheetContent by remember() {
+ mutableStateOf<@Composable () -> Unit>({})
+ }
+
+ CompositionLocalProvider(
+ LocalBottomDialogState provides BottomState(
+ sheetState = modalState,
+ setContent = {
+ sheetContent = it
+ }
+ )
) {
- val multiplePermissionsState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- rememberMultiplePermissionsState(
- listOf(
- android.Manifest.permission.BLUETOOTH_SCAN,
- android.Manifest.permission.BLUETOOTH_CONNECT
- )
- )
- } else {
- rememberMultiplePermissionsState(
- listOf()
- )
- }
+ ModalBottomSheetLayout(
+ sheetShape = RoundedCornerShape(
+ topStart = 25.dp,
+ topEnd = 25.dp
+ ),
+ sheetElevation = 0.dp,
+ sheetState = modalState,
+ sheetContent = {
- if(multiplePermissionsState.allPermissionsGranted) {
+ val scope = rememberCoroutineScope()
- MainScreen()
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
- } else {
+ Surface(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+
+ Column() {
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .size(
+ width = 54.dp,
+ height = 5.dp
+ )
+
+ ) {}
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ sheetContent()
+
+ Spacer(modifier = Modifier.navigationBarsPadding())
+ }
+
+ }
+ }
+
+ BackHandler(modalState.isVisible) {
+ scope.launch { modalState.hide() }
+ }
+
+ },
+ content = {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .navigationBarsPadding(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+
+ val multiplePermissionsState =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ rememberMultiplePermissionsState(
+ listOf(
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT
+ )
+ )
+ } else {
+ rememberMultiplePermissionsState(
+ listOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ )
+ )
+ }
+
+ if (multiplePermissionsState.allPermissionsGranted) {
+
+ MainScreen()
+
+ } else {
+
+ LaunchedEffect(multiplePermissionsState) {
+ multiplePermissionsState.launchMultiplePermissionRequest()
+ }
+
+ }
+
+ }
- LaunchedEffect(multiplePermissionsState){
- multiplePermissionsState.launchMultiplePermissionRequest()
}
-
- }
-
+ )
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt b/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt
new file mode 100644
index 0000000..06706b3
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt
@@ -0,0 +1,51 @@
+package llc.arma.ble.app.ui.common
+
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.remember
+
+val LocalBottomDialogState = compositionLocalOf { null }
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun rememberBottomDialogState(): BottomDialogState {
+
+ val state = LocalBottomDialogState.current
+
+ return remember {
+ BottomDialogState(
+ sheetState = state?.sheetState,
+ setContent = state?.setContent ?: { }
+ )
+ }
+}
+
+class BottomState @OptIn(ExperimentalMaterialApi::class) constructor(
+ val sheetState: ModalBottomSheetState?,
+ val setContent: (@Composable () -> Unit) -> Unit,
+)
+
+class BottomDialogState @OptIn(ExperimentalMaterialApi::class) constructor(
+ private val sheetState: ModalBottomSheetState?,
+ val setContent: (@Composable () -> Unit) -> Unit,
+) {
+
+ @OptIn(ExperimentalMaterialApi::class)
+ suspend fun show(
+ content: @Composable () -> Unit
+ ){
+ setContent(content)
+ if(sheetState?.currentValue != ModalBottomSheetValue.Expanded)
+ sheetState?.show()
+ }
+
+ @OptIn(ExperimentalMaterialApi::class)
+ suspend fun hide(){
+ sheetState?.hide()
+ setContent { }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt
index c541f3f..111e144 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt
@@ -3,20 +3,36 @@ package llc.arma.ble.app.ui.screen.beacon
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
class BeaconContract {
sealed class Event : ViewEvent {
+ object OnWriteBle : Event()
+
+ object OnHideWriteBlePreview : Event()
+
+ object OnShowWriteBlePreview : Event()
+
+ object OnPowerEdit : Event()
+
data class OnBleChanged(
val ble: Ble.Beacon
) : Event()
+ data class OnPowerChanged(
+ val tx: BleView.BleState.TX
+ ) : Event()
+
data class OnTxChanged(val tx: Int) : Event()
object OnNavigateUpClicked : Event()
+ object OnChangePassword : Event()
+
}
sealed class State : ViewState {
@@ -24,15 +40,45 @@ class BeaconContract {
object Loading : State()
data class Display(
- val beacon: Ble.Beacon
- ) : State()
+ val origin: Ble.Beacon,
+ val beacon: BleView.Beacon,
+ val writeState: WriteState?
+ ) : State() {
+
+ sealed class WriteState {
+
+ data class DisplayPreview(
+ val writeRequest: Ble.Beacon.WriteRequest
+ ) : WriteState()
+
+ data class Writing(
+ val writeRequest: Ble.Beacon.WriteRequest
+ ) : WriteState()
+
+ object Success : WriteState()
+
+ object Failure : WriteState()
+
+ }
+
+ }
}
sealed class Effect : ViewSideEffect {
+ object ShowPowerPicker : Effect()
+
+ object HidePowerPicker : Effect()
+
+ object HideWriteBlePreview : Effect()
+
+ object ShowWriteBlePreview : Effect()
+
sealed class Navigation : Effect() {
+ object NavigateToChangePassword : Navigation()
+
object NavigateUp : Navigation()
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt
index 708c882..3a51bdb 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt
@@ -1,30 +1,33 @@
package llc.arma.ble.app.ui.screen.beacon
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowRight
-import androidx.compose.material.icons.rounded.Refresh
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import llc.arma.ble.app.ui.screen.BleInfoView
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.rememberBottomDialogState
+import llc.arma.ble.app.ui.screen.beacon.view.DisplayState
+import llc.arma.ble.app.ui.screen.beacon.view.PowerEdit
+import llc.arma.ble.app.ui.screen.thermometer.localizedName
import llc.arma.ble.domain.model.Ble
-import llc.arma.ble.domain.model.BleInfo
-@OptIn(ExperimentalMaterial3Api::class)
+enum class SheetPage {
+ WRITE, POWER_EDIT
+}
+
@Composable
fun BeaconScreen(
ble: Ble.Beacon,
@@ -34,10 +37,32 @@ fun BeaconScreen(
val viewModel = hiltViewModel()
val state = viewModel.viewState.value
+ var sheetPage by rememberSaveable {
+ mutableStateOf(null)
+ }
+
+ val bottomDialog = rememberBottomDialogState()
+
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is BeaconContract.Effect.Navigation -> onNavigationEvent(it)
+ BeaconContract.Effect.HideWriteBlePreview -> launch {
+ sheetPage = null
+ }
+ BeaconContract.Effect.ShowWriteBlePreview -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.WRITE
+ }
+ BeaconContract.Effect.HidePowerPicker -> launch {
+ sheetPage = null
+ }
+ BeaconContract.Effect.ShowPowerPicker -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.POWER_EDIT
+ }
}
}.launchIn(this)
}
@@ -46,29 +71,291 @@ fun BeaconScreen(
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
}
- Column {
-
- CenterAlignedTopAppBar(
- navigationIcon = {
- IconButton(
- onClick = {
- viewModel.setEvent(BeaconContract.Event.OnNavigateUpClicked)
- },
- content = {
- Icon(
- imageVector = Icons.Rounded.ArrowBack,
- contentDescription = null
- )
+ LaunchedEffect(sheetPage){
+ when(sheetPage){
+ SheetPage.WRITE -> bottomDialog.show {
+
+ val scope = rememberCoroutineScope()
+
+ val currentState = viewModel.viewState.value
+
+ if(currentState is BeaconContract.State.Display) {
+
+ Column() {
+
+ when (currentState.writeState) {
+ is BeaconContract.State.Display.WriteState.DisplayPreview -> {
+
+ Text(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ text = "Записать изменения?",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ currentState.writeState.writeRequest.tx?.let {
+ Box(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Мощность"
+ )
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${currentState.origin.state.tx.localizedName} db -> ${it.localizedName} db"
+ )
+
+ }
+
+ }
+
+ }
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ viewModel.setEvent(BeaconContract.Event.OnWriteBle)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Записать"
+ )
+
+ }
+
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ scope.launch {
+ viewModel.setEvent(BeaconContract.Event.OnHideWriteBlePreview)
+ }
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+
+ }
+ is BeaconContract.State.Display.WriteState.Writing -> {
+
+ Box {
+
+ Column() {
+
+ Text(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ text = "Запись",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ CircularProgressIndicator(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = 48.dp)
+ )
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ scope.launch {
+ viewModel.setEvent(BeaconContract.Event.OnHideWriteBlePreview)
+ }
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ BeaconContract.State.Display.WriteState.Success -> {
+
+ Box {
+
+ Column {
+
+ Text(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ text = "Запись завершена",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ onClick = {
+ scope.launch {
+ viewModel.setEvent(BeaconContract.Event.OnHideWriteBlePreview)
+ }
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ BeaconContract.State.Display.WriteState.Failure -> {
+
+ Box {
+
+ Column {
+
+ Text(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ text = "Ошибка записи",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ onClick = {
+ scope.launch {
+ viewModel.setEvent(BeaconContract.Event.OnHideWriteBlePreview)
+ }
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ else -> {}
+ }
+
+ Spacer(modifier = Modifier.height(48.dp))
+
}
- )
- },
- title = {
- if (state is BeaconContract.State.Display) Text(text = state.beacon.info.name)
+
+ }
+
}
- )
+ SheetPage.POWER_EDIT -> bottomDialog.show {
+ val currentState = viewModel.viewState.value
+
+ if(currentState is BeaconContract.State.Display) {
+ PowerEdit(
+ state = currentState.beacon,
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
+ }
+ }
+ else -> {
+ bottomDialog.hide()
+ }
+ }
+ }
+
+ Column {
when(state){
- is BeaconContract.State.Display -> DisplayState(state.beacon)
+ is BeaconContract.State.Display -> DisplayState(
+ onEvent = {
+ viewModel.setEvent(it)
+ },
+ ble = state.beacon
+ )
is BeaconContract.State.Loading -> LoadingState()
}
@@ -85,103 +372,4 @@ private fun LoadingState(){
}
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun DisplayState(ble: Ble.Beacon){
-
- Column {
-
- LazyColumn(
- modifier = Modifier.weight(1f),
- content = {
-
- item {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
- BleInfoView(bleInfo = ble.info)
- }
-
- }
-
- item {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ){
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .clickable { }
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Мощность"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "-40 db"
- )
-
- }
-
- Icon(
- imageVector = Icons.Rounded.KeyboardArrowDown,
- contentDescription = null
- )
-
- }
-
- }
-
- }
-
- }
-
- )
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .height(50.dp),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.primaryContainer,
- onClick = {
-
- }
- ) {
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- Text(
- modifier = Modifier.align(Alignment.Center),
- color = MaterialTheme.colorScheme.background,
- style = MaterialTheme.typography.labelLarge,
- text = "Сохранить"
- )
-
- }
-
- }
-
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt
index d3a4b99..bccebca 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt
@@ -1,18 +1,27 @@
package llc.arma.ble.app.ui.screen.beacon
import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.app.ui.mapper.BleMapper
+import llc.arma.ble.app.ui.mapper.BleViewMapper
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
+import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class BeaconViewModel @Inject constructor(
+ private val bleMapper: BleMapper,
+ private val writeBle: WriteBle,
+ private val bleViewMapper: BleViewMapper
) : BaseViewModel() {
override fun setInitialState() = BeaconContract.State.Loading
@@ -22,9 +31,41 @@ class BeaconViewModel @Inject constructor(
is BeaconContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnBleChanged -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnChangePassword -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnWriteBle -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnPowerEdit -> reduce(viewState.value, event)
}
}
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnPowerChanged
+ ) {
+
+ if(state is BeaconContract.State.Display) {
+
+ state.beacon.state.tx = event.tx
+
+ }
+
+ setEffect {
+ BeaconContract.Effect.HidePowerPicker
+ }
+
+ }
+
+
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnPowerEdit
+ ) {
+ setEffect { BeaconContract.Effect.ShowPowerPicker }
+ }
+
+
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUpClicked
@@ -45,9 +86,104 @@ class BeaconViewModel @Inject constructor(
) {
setState {
BeaconContract.State.Display(
- event.ble
+ origin = event.ble,
+ beacon = bleMapper.map(event.ble) as BleView.Beacon,
+ writeState = null
)
}
}
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnChangePassword
+ ) {
+ setEffect {
+ BeaconContract.Effect.Navigation.NavigateToChangePassword
+ }
+ }
+
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnHideWriteBlePreview
+ ) {
+ setEffect {
+ BeaconContract.Effect.HideWriteBlePreview
+ }
+ }
+
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnShowWriteBlePreview
+ ) {
+
+ if(state is BeaconContract.State.Display){
+
+ val newBle = bleViewMapper.map(state.beacon) as Ble.Beacon
+
+ val writeRequest = Ble.Beacon.WriteRequest(
+ tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx
+ )
+
+ setState {
+ state.copy(
+ writeState = BeaconContract.State.Display.WriteState.DisplayPreview(
+ writeRequest
+ )
+ )
+ }
+
+ setEffect {
+ BeaconContract.Effect.ShowWriteBlePreview
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnWriteBle
+ ) {
+
+ if(state is BeaconContract.State.Display){
+
+ state.writeState?.let {
+
+ if(it is BeaconContract.State.Display.WriteState.DisplayPreview) {
+
+ viewModelScope.launch {
+
+ setState {
+ state.copy(
+ writeState = BeaconContract.State.Display.WriteState.Writing(it.writeRequest)
+ )
+ }
+
+ writeBle(state.beacon.info.serial, it.writeRequest).fold(
+ onSuccess = {
+ setState {
+ state.copy(
+ writeState = BeaconContract.State.Display.WriteState.Success
+ )
+ }
+ },
+ onFailure = {
+ setState {
+ state.copy(
+ writeState = BeaconContract.State.Display.WriteState.Failure
+ )
+ }
+ }
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/view/DisplayState.kt
new file mode 100644
index 0000000..b39bab8
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/view/DisplayState.kt
@@ -0,0 +1,158 @@
+package llc.arma.ble.app.ui.screen.beacon.view
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.KeyboardArrowRight
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.BleInfoView
+import llc.arma.ble.app.ui.screen.beacon.BeaconContract
+
+@Composable
+fun DisplayState(
+ onEvent: (BeaconContract.Event) -> Unit,
+ ble: BleView.Beacon
+) {
+
+ Column() {
+
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .weight(1f)
+ ) {
+
+ Box(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 8.dp
+ )
+ ) {
+ BleInfoView(bleInfo = ble.info)
+ }
+
+ Column(
+ modifier = Modifier,
+ content = {
+
+ Box(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .clickable {
+ onEvent(BeaconContract.Event.OnPowerEdit)
+ }
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Мощность"
+ )
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${ble.state.tx.value} db"
+ )
+
+ }
+
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+
+ }
+
+ }
+
+ Box(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .clickable {
+ onEvent(BeaconContract.Event.OnChangePassword)
+ }
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Изменить пароль"
+ )
+
+ }
+
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+
+ }
+
+ }
+
+ }
+ )
+
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onEvent(BeaconContract.Event.OnShowWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Сохранить"
+ )
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/view/PowerEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/view/PowerEdit.kt
new file mode 100644
index 0000000..cc8d363
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/view/PowerEdit.kt
@@ -0,0 +1,97 @@
+package llc.arma.ble.app.ui.screen.beacon.view
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.beacon.BeaconContract
+import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
+
+@Composable
+fun PowerEdit(
+ state: BleView.Beacon,
+ onEvent: (BeaconContract.Event) -> Unit,
+){
+
+ var value by remember(state.state.tx) {
+ mutableStateOf(state.state.tx)
+ }
+
+ Column(
+ modifier = Modifier
+ ) {
+
+ Text(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ text = "Мощность",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BleView.BleState.TX.values().forEach {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp))
+ .clickable { value = it }
+ .padding(4.dp)
+ ) {
+
+ RadioButton(
+ selected = it == value,
+ onClick = { value = it }
+ )
+
+ Text(text = it.value.toString() + " db")
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onEvent(
+ BeaconContract.Event.OnPowerChanged(
+ value
+ )
+ )
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Применить"
+ )
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt
index 6fa7094..f972f34 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt
@@ -13,6 +13,8 @@ class ConnectionContract {
sealed class Event : ViewEvent {
+ object RefreshBle : Event()
+
object OnNavigateUp : Event()
data class OnBeaconNavigationEvent(
@@ -44,6 +46,11 @@ class ConnectionContract {
sealed class Navigation : Effect() {
object NavigateUp : Navigation()
+
+ data class NavigateToChangePassword(
+ val serial: String
+ ) : Navigation()
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt
index 3f294fd..8b18f39 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt
@@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
@@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.beacon.BeaconScreen
+import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
import llc.arma.ble.app.ui.screen.thermometer.ThermometerScreen
import llc.arma.ble.domain.model.Ble
@@ -82,16 +84,20 @@ fun ConnectionScreen(
)
when (state) {
- is ConnectionContract.State.DisplayException -> DisplayException(state.exception)
+ is ConnectionContract.State.DisplayException -> DisplayException(
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
is ConnectionContract.State.Loading -> LoadingState()
is ConnectionContract.State.Display -> {
when(state.ble){
- is Ble.Beacon -> {}/*BeaconScreen(
+ is Ble.Beacon -> BeaconScreen(
ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(ConnectionContract.Event.OnBeaconNavigationEvent(it))
}
- )*/
+ )
is Ble.Thermometer -> {
Column(modifier = Modifier.weight(1f)) {
@@ -100,9 +106,7 @@ fun ConnectionScreen(
ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
- ConnectionContract.Event.OnThermometerNavigationEvent(
- it
- )
+ ConnectionContract.Event.OnThermometerNavigationEvent(it)
)
}
)
@@ -135,10 +139,50 @@ private fun LoadingState(){
@Composable
private fun DisplayException(
- exception: GetBleBySerial.GetBleException
+ onEvent: (ConnectionContract.Event) -> Unit
){
- Column {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.align(Alignment.Center)
+ ) {
+
+ Text(
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleMedium,
+ text = "Неудалось соединится с устройством"
+ )
+
+ Spacer(modifier = Modifier.height(18.dp))
+
+ Surface(
+ modifier = Modifier
+ .height(42.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onEvent(ConnectionContract.Event.RefreshBle)
+ }
+ ) {
+
+ Box(modifier = Modifier.padding(horizontal = 16.dp)) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Повторить"
+ )
+
+ }
+
+ }
+
+ }
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt
index cf135ea..35638b0 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt
@@ -17,19 +17,92 @@ import javax.inject.Inject
@HiltViewModel
class ConnectionViewModel @Inject constructor(
- savedStateHandle: SavedStateHandle,
- getBleBySerial: GetBleBySerial,
- private val writeBle: WriteBle,
- private val bleMapper: BleMapper,
- private val bleViewMapper: BleViewMapper
+ private val savedStateHandle: SavedStateHandle,
+ private val getBleBySerial: GetBleBySerial,
) : BaseViewModel() {
init {
+ refreshBle()
+ }
+ override fun setInitialState() = ConnectionContract.State.Loading
+
+ override fun handleEvents(event: ConnectionContract.Event) {
+ when(event){
+ is ConnectionContract.Event.OnBeaconNavigationEvent -> reduce(viewState.value, event)
+ is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event)
+ is ConnectionContract.Event.RefreshBle -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: ConnectionContract.State,
+ event: ConnectionContract.Event.OnBeaconNavigationEvent
+ ) {
+ when(event.event){
+ BeaconContract.Effect.Navigation.NavigateUp -> {
+ setEffect {
+ ConnectionContract.Effect.Navigation.NavigateUp
+ }
+ }
+ BeaconContract.Effect.Navigation.NavigateToChangePassword -> {
+ setEffect {
+ ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get("serial")!!)
+ }
+ }
+ }
+ }
+
+ private fun reduce(
+ state: ConnectionContract.State,
+ event: ConnectionContract.Event.OnThermometerNavigationEvent
+ ) {
+ when(event.event){
+ ThermometerContract.Effect.Navigation.NavigateUp -> {
+ setEffect {
+ ConnectionContract.Effect.Navigation.NavigateUp
+ }
+ }
+ ThermometerContract.Effect.Navigation.NavigateToChangePassword -> {
+ setEffect {
+ ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get("serial")!!)
+ }
+ }
+ }
+ }
+
+ private fun reduce(
+ state: ConnectionContract.State,
+ event: ConnectionContract.Event.OnNavigateUp
+ ) {
+
+ setEffect {
+ ConnectionContract.Effect.Navigation.NavigateUp
+ }
+
+ }
+
+ private fun reduce(
+ state: ConnectionContract.State,
+ event: ConnectionContract.Event.RefreshBle
+ ) {
+
+ refreshBle()
+
+ }
+
+ private fun refreshBle(){
val serial = savedStateHandle.get("serial")
if(serial != null){
+
viewModelScope.launch {
+
+ setState {
+ ConnectionContract.State.Loading
+ }
+
getBleBySerial(serial).fold(
onSuccess = {
@@ -50,54 +123,6 @@ class ConnectionViewModel @Inject constructor(
} else {
throw IllegalArgumentException("serial arg must not be null")
}
-
- }
-
- override fun setInitialState() = ConnectionContract.State.Loading
-
- override fun handleEvents(event: ConnectionContract.Event) {
- when(event){
- is ConnectionContract.Event.OnBeaconNavigationEvent -> reduce(viewState.value, event)
- is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event)
- is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event)
- }
- }
-
- private fun reduce(
- state: ConnectionContract.State,
- event: ConnectionContract.Event.OnBeaconNavigationEvent
- ) {
- when(event.event){
- BeaconContract.Effect.Navigation.NavigateUp -> {
- setEffect {
- ConnectionContract.Effect.Navigation.NavigateUp
- }
- }
- }
- }
-
- private fun reduce(
- state: ConnectionContract.State,
- event: ConnectionContract.Event.OnThermometerNavigationEvent
- ) {
- when(event.event){
- ThermometerContract.Effect.Navigation.NavigateUp -> {
- setEffect {
- ConnectionContract.Effect.Navigation.NavigateUp
- }
- }
- }
- }
-
- private fun reduce(
- state: ConnectionContract.State,
- event: ConnectionContract.Event.OnNavigateUp
- ) {
-
- setEffect {
- ConnectionContract.Effect.Navigation.NavigateUp
- }
-
}
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt
index a0c45b4..eedfd3b 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt
@@ -1,8 +1,10 @@
package llc.arma.ble.app.ui.screen.main
import androidx.compose.runtime.Composable
+import androidx.compose.ui.window.DialogProperties
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
+import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import llc.arma.ble.app.ui.screen.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.thermometer.ThermometerScreen
@@ -11,6 +13,8 @@ import llc.arma.ble.app.ui.screen.ble.BleListContract
import llc.arma.ble.app.ui.screen.ble.BleListScreen
import llc.arma.ble.app.ui.screen.connection.ConnectionContract
import llc.arma.ble.app.ui.screen.connection.ConnectionScreen
+import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
+import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen
@Composable
fun MainScreen() {
@@ -45,6 +49,7 @@ fun MainScreen() {
onNavigationEvent = {
when(it){
ConnectionContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
+ is ConnectionContract.Effect.Navigation.NavigateToChangePassword -> controller.navigate("change_password/${it.serial}")
}
}
)
@@ -52,6 +57,18 @@ fun MainScreen() {
}
)
+ dialog(
+ route = "change_password/{serial}",
+ dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
+ content = {
+ ChangePasswordScreen {
+ when(it){
+ is ChangePasswordContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
+ }
+ }
+ }
+ )
+
}
)
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt
new file mode 100644
index 0000000..01f94a2
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt
@@ -0,0 +1,62 @@
+package llc.arma.ble.app.ui.screen.password
+
+import llc.arma.ble.app.ui.common.ViewEvent
+import llc.arma.ble.app.ui.common.ViewSideEffect
+import llc.arma.ble.app.ui.common.ViewState
+
+class ChangePasswordContract {
+
+ sealed class Event : ViewEvent {
+
+ data class OnPasswordChanged(
+ val password: String
+ ) : Event()
+
+ data class OnRePasswordChanged(
+ val password: String
+ ) : Event()
+
+ object OnChange : Event()
+
+ object OnNavigateUp : Event()
+
+ }
+
+ data class State(
+ val password: String,
+ val rePassword: String,
+ val exception: ValidationException?,
+ val loading: LoadingState?
+ ) : ViewState {
+
+ sealed class LoadingState {
+
+ object Loading : LoadingState()
+
+ object Success : LoadingState()
+
+ object Failure : LoadingState()
+
+ }
+
+ sealed class ValidationException {
+
+ object PasswordsNotMatch : ValidationException()
+
+ object WrongLength : ValidationException()
+
+ }
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ object NavigateUp : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt
new file mode 100644
index 0000000..1a1041e
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt
@@ -0,0 +1,270 @@
+package llc.arma.ble.app.ui.screen.password
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Visibility
+import androidx.compose.material.icons.rounded.VisibilityOff
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import llc.arma.ble.app.ui.screen.password.view.Loading
+import llc.arma.ble.app.ui.screen.password.view.Result
+import llc.arma.ble.app.ui.screen.thermometer.view.LoadingState
+
+@Composable
+fun ChangePasswordScreen(
+ onNavigationEvent: (ChangePasswordContract.Effect.Navigation) -> Unit
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect("effect"){
+ viewModel.effect.onEach {
+ when(it){
+ is ChangePasswordContract.Effect.Navigation -> onNavigationEvent(it)
+ }
+ }.launchIn(this)
+ }
+
+ Surface(
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(24.dp),
+ ) {
+
+ Column {
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ style = MaterialTheme.typography.titleLarge,
+ text = "Изменение пароля",
+ textAlign = TextAlign.Center
+ )
+
+ if(state.loading != null){
+
+ when(state.loading){
+
+ ChangePasswordContract.State.LoadingState.Loading -> {
+ Loading {
+ viewModel.setEvent(it)
+ }
+ }
+ ChangePasswordContract.State.LoadingState.Failure,
+ ChangePasswordContract.State.LoadingState.Success -> {
+ Result(
+ onEvent = {
+ viewModel.setEvent(it)
+ },
+ state = state.loading
+ )
+ }
+ }
+
+ } else {
+
+ Column(
+ modifier = Modifier.padding(20.dp)
+ ) {
+
+
+ var passwordVisibility by remember {
+ mutableStateOf(false)
+ }
+ var rePasswordVisibility by remember {
+ mutableStateOf(false)
+ }
+
+ @Composable
+ fun TrailingPasswordIcon(
+ visible: Boolean,
+ onClick: () -> Unit
+ ) {
+ IconButton(onClick = onClick) {
+ Icon(
+ contentDescription = null,
+ imageVector = if (visible) {
+ Icons.Rounded.Visibility
+ } else {
+ Icons.Rounded.VisibilityOff
+ }
+ )
+ }
+ }
+
+ val isError = state.exception != null
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ visualTransformation = if (passwordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
+ value = state.password,
+ onValueChange = {
+ viewModel.setEvent(ChangePasswordContract.Event.OnPasswordChanged(it))
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.NumberPassword,
+ imeAction = ImeAction.Next),
+ trailingIcon = {
+ TrailingPasswordIcon(visible = passwordVisibility) {
+ passwordVisibility = passwordVisibility.not()
+ }
+ },
+ isError = isError,
+ label = {
+ Text(text = "Пароль")
+ },
+ supportingText = {
+ Row() {
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${state.password.length}/6"
+ )
+ }
+ }
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ visualTransformation = if (rePasswordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
+ value = state.rePassword,
+ onValueChange = {
+ viewModel.setEvent(ChangePasswordContract.Event.OnRePasswordChanged(it))
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
+ trailingIcon = {
+ TrailingPasswordIcon(visible = rePasswordVisibility) {
+ rePasswordVisibility = rePasswordVisibility.not()
+ }
+ },
+ label = {
+ Text(text = "Повторите пароль")
+ },
+ isError = isError,
+ supportingText = {
+
+ Row() {
+
+ if (isError) {
+ val text = when (state.exception) {
+ is ChangePasswordContract.State.ValidationException.WrongLength -> "Неверная длинна"
+ is ChangePasswordContract.State.ValidationException.PasswordsNotMatch -> "Пароли не совпадают"
+ null -> ""
+ }
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.error,
+ text = text
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${state.rePassword.length}/6"
+ )
+
+ }
+
+ }
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Box(
+ modifier = Modifier
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ viewModel.setEvent(ChangePasswordContract.Event.OnChange)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Применить"
+ )
+
+ }
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Box(
+ modifier = Modifier
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ viewModel.setEvent(ChangePasswordContract.Event.OnNavigateUp)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt
new file mode 100644
index 0000000..1dd9b55
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt
@@ -0,0 +1,125 @@
+package llc.arma.ble.app.ui.screen.password
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.domain.usecase.ChangeBlePassword
+import javax.inject.Inject
+
+@HiltViewModel
+class ChangePasswordViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val changeBlePassword: ChangeBlePassword
+) : BaseViewModel() {
+
+ override fun setInitialState() = ChangePasswordContract.State("", "", null, null)
+
+ override fun handleEvents(event: ChangePasswordContract.Event) {
+ when(event){
+ is ChangePasswordContract.Event.OnPasswordChanged -> reduce(viewState.value, event)
+ is ChangePasswordContract.Event.OnRePasswordChanged -> reduce(viewState.value, event)
+ is ChangePasswordContract.Event.OnChange -> reduce(viewState.value, event)
+ is ChangePasswordContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: ChangePasswordContract.State,
+ event: ChangePasswordContract.Event.OnPasswordChanged
+ ) {
+
+ setState {
+ copy(
+ password = event.password
+ )
+ }
+
+ }
+
+ private fun reduce(
+ state: ChangePasswordContract.State,
+ event: ChangePasswordContract.Event.OnRePasswordChanged
+ ) {
+
+ setState {
+ copy(
+ rePassword = event.password
+ )
+ }
+
+ }
+
+ private fun reduce(
+ state: ChangePasswordContract.State,
+ event: ChangePasswordContract.Event.OnChange
+ ) {
+
+ if(state.password.length != 6 || state.rePassword.length != 6){
+ setState {
+ state.copy(
+ exception = ChangePasswordContract.State.ValidationException.WrongLength
+ )
+ }
+ } else {
+
+ if(state.password != state.rePassword){
+ setState {
+ state.copy(
+ exception = ChangePasswordContract.State.ValidationException.PasswordsNotMatch
+ )
+ }
+ } else {
+
+ viewModelScope.launch {
+
+ setState {
+ state.copy(
+ loading = ChangePasswordContract.State.LoadingState.Loading,
+ exception = null
+ )
+ }
+
+ changeBlePassword.invoke(
+ state.password,
+ savedStateHandle.get("serial")!!
+ ).fold(
+ onSuccess = {
+ setState {
+ state.copy(
+ loading = ChangePasswordContract.State.LoadingState.Success,
+ exception = null
+ )
+ }
+ },
+ onFailure = {
+ setState {
+ state.copy(
+ loading = ChangePasswordContract.State.LoadingState.Failure,
+ exception = null
+ )
+ }
+ }
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: ChangePasswordContract.State,
+ event: ChangePasswordContract.Event.OnNavigateUp
+ ) {
+
+ setEffect {
+ ChangePasswordContract.Effect.Navigation.NavigateUp
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Display.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Display.kt
new file mode 100644
index 0000000..d8d4daf
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Display.kt
@@ -0,0 +1,205 @@
+package llc.arma.ble.app.ui.screen.password.view
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Visibility
+import androidx.compose.material.icons.rounded.VisibilityOff
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
+
+@Composable
+fun Display(
+ onEvent: (ChangePasswordContract.Event) -> Unit,
+ state: ChangePasswordContract.State
+){
+
+ Column(
+ modifier = Modifier.padding(20.dp)
+ ) {
+
+
+ var passwordVisibility by remember {
+ mutableStateOf(false)
+ }
+ var rePasswordVisibility by remember {
+ mutableStateOf(false)
+ }
+
+ @Composable
+ fun TrailingPasswordIcon(
+ visible: Boolean,
+ onClick: () -> Unit
+ ) {
+ IconButton(onClick = onClick) {
+ Icon(
+ contentDescription = null,
+ imageVector = if (visible) {
+ Icons.Rounded.Visibility
+ } else {
+ Icons.Rounded.VisibilityOff
+ }
+ )
+ }
+ }
+
+ val isError = state.exception != null
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ visualTransformation = if (passwordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
+ value = state.password,
+ onValueChange = {
+ onEvent(ChangePasswordContract.Event.OnPasswordChanged(it))
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.NumberPassword,
+ imeAction = ImeAction.Next),
+ trailingIcon = {
+ TrailingPasswordIcon(visible = passwordVisibility) {
+ passwordVisibility = passwordVisibility.not()
+ }
+ },
+ isError = isError,
+ label = {
+ Text(text = "Пароль")
+ },
+ supportingText = {
+ Row() {
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${state.password.length}/6"
+ )
+ }
+ }
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ visualTransformation = if (rePasswordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
+ value = state.rePassword,
+ onValueChange = {
+ onEvent(ChangePasswordContract.Event.OnRePasswordChanged(it))
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
+ trailingIcon = {
+ TrailingPasswordIcon(visible = rePasswordVisibility) {
+ rePasswordVisibility = rePasswordVisibility.not()
+ }
+ },
+ label = {
+ Text(text = "Повторите пароль")
+ },
+ isError = isError,
+ supportingText = {
+
+ Row() {
+
+ if (isError) {
+ val text = when (state.exception) {
+ is ChangePasswordContract.State.ValidationException.WrongLength -> "Неверная длинна"
+ is ChangePasswordContract.State.ValidationException.PasswordsNotMatch -> "Пароли не совпадают"
+ null -> ""
+ }
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.error,
+ text = text
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${state.rePassword.length}/6"
+ )
+
+ }
+
+ }
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Box(
+ modifier = Modifier
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onEvent(ChangePasswordContract.Event.OnChange)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Применить"
+ )
+
+ }
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Box(
+ modifier = Modifier
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ onEvent(ChangePasswordContract.Event.OnNavigateUp)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt
new file mode 100644
index 0000000..4e1556f
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt
@@ -0,0 +1,67 @@
+package llc.arma.ble.app.ui.screen.password.view
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Visibility
+import androidx.compose.material.icons.rounded.VisibilityOff
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
+
+@Composable
+fun Loading(
+ onEvent: (ChangePasswordContract.Event) -> Unit
+) {
+
+ Column(
+ modifier = Modifier.padding(20.dp)
+ ) {
+
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Box(
+ modifier = Modifier
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ onEvent(ChangePasswordContract.Event.OnNavigateUp)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Result.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Result.kt
new file mode 100644
index 0000000..332816a
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Result.kt
@@ -0,0 +1,115 @@
+package llc.arma.ble.app.ui.screen.password.view
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.CircularProgressIndicator
+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.res.painterResource
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
+
+@Composable
+fun Result(
+ onEvent: (ChangePasswordContract.Event) -> Unit,
+ state: ChangePasswordContract.State.LoadingState
+) {
+
+ Column(
+ modifier = Modifier.padding(20.dp)
+ ) {
+
+ when(state){
+ ChangePasswordContract.State.LoadingState.Failure -> {
+
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+
+ Image(
+ modifier = Modifier
+ .size(125.dp)
+ .align(Alignment.Center),
+ painter = painterResource(llc.arma.ble.R.drawable.ic_error),
+ contentDescription = null
+ )
+
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Ошибка записи"
+ )
+ }
+ ChangePasswordContract.State.LoadingState.Success -> {
+
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+
+ Image(
+ modifier = Modifier
+ .size(125.dp)
+ .align(Alignment.Center),
+ painter = painterResource(llc.arma.ble.R.drawable.ic_done),
+ contentDescription = null
+ )
+
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Успешно завершено"
+ )
+ }
+ else -> {}
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Box(
+ modifier = Modifier
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ onClick = {
+ onEvent(ChangePasswordContract.Event.OnNavigateUp)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt
index 8f78d73..c772b8b 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt
@@ -20,6 +20,8 @@ class ThermometerContract {
object OnHideTemperatureHistory : Event()
+ object OnChangePassword : Event()
+
data class OnSaveHistoryChanged(
val saveHistory: Boolean
) : Event()
@@ -68,6 +70,8 @@ class ThermometerContract {
object Success : WriteState()
+ object Failure : WriteState()
+
}
}
@@ -88,10 +92,16 @@ class ThermometerContract {
object HidePowerPicker : Effect()
+ object ShowWriteBle : Effect()
+
+ object HideWriteBle : Effect()
+
sealed class Navigation : Effect() {
object NavigateUp : Navigation()
+ object NavigateToChangePassword : Navigation()
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt
index b4e53fc..9389907 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt
@@ -1,32 +1,41 @@
package llc.arma.ble.app.ui.screen.thermometer
+import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.*
import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import llc.arma.ble.R
+import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.thermometer.view.*
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
- INTERVAL, POWER, TEMPERATURE_HISTORY
+ INTERVAL, POWER, TEMPERATURE_HISTORY, WRITE
}
-private val Boolean.localizedName: String
+val Boolean.localizedName: String
get() {
return if(this){
"Включено"
@@ -35,7 +44,7 @@ private val Boolean.localizedName: String
}
}
-private val Ble.BleState.TX.localizedName: String
+val Ble.BleState.TX.localizedName: String
get() {
return when(this){
Ble.BleState.TX.MINUS_40 -> -40
@@ -51,48 +60,133 @@ private val Ble.BleState.TX.localizedName: String
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun ThermometerScreen(
ble: Ble.Thermometer,
onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
) {
- var sheetPage by remember {
+ var sheetPage by rememberSaveable {
mutableStateOf(null)
}
val viewModel = hiltViewModel()
val state = viewModel.viewState.value
- val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ val bottomDialog = rememberBottomDialogState()
+
+ LaunchedEffect(sheetPage){
+ when(sheetPage){
+ SheetPage.INTERVAL -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if(currentState is ThermometerContract.State.Display) {
+ IntervalEdit(
+ state = currentState.thermometer,
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
+ }
+
+ }
+ SheetPage.POWER -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if(currentState is ThermometerContract.State.Display) {
+ PowerEdit(
+ state = currentState.thermometer,
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
+ }
+
+ }
+ SheetPage.TEMPERATURE_HISTORY -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if (currentState is ThermometerContract.State.Display) {
+ TemperatureHistory(
+ ble = currentState.thermometer.info
+ )
+ }
+ }
+ SheetPage.WRITE -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if (currentState is ThermometerContract.State.Display) {
+
+ currentState.writeState?.let {
+
+ Write(
+ state = it,
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
+
+ }
+
+ }
+ }
+ else -> {
+ bottomDialog.hide()
+ }
+ }
+ }
+
val writeSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
- is ThermometerContract.Effect.Navigation -> onNavigationEvent(it)
- is ThermometerContract.Effect.HideIntervalPicker -> launch {
- bottomSheetState.hide()
+ is ThermometerContract.Effect.Navigation -> {
sheetPage = null
+ onNavigationEvent(it)
+ }
+ is ThermometerContract.Effect.HideIntervalPicker -> launch {
+ sheetPage = null
+ delay(100)
}
is ThermometerContract.Effect.ShowIntervalPicker -> launch {
+ sheetPage = null
+ delay(100)
sheetPage = SheetPage.INTERVAL
}
is ThermometerContract.Effect.HidePowerPicker -> launch {
- bottomSheetState.hide()
sheetPage = null
+ delay(100)
}
is ThermometerContract.Effect.ShowPowerPicker -> launch {
+ sheetPage = null
+ delay(100)
sheetPage = SheetPage.POWER
}
is ThermometerContract.Effect.HideTemperatureHistory -> launch {
- bottomSheetState.hide()
sheetPage = null
+ delay(100)
}
is ThermometerContract.Effect.ShowTemperatureHistory -> launch {
+ sheetPage = null
+ delay(100)
sheetPage = SheetPage.TEMPERATURE_HISTORY
}
+ is ThermometerContract.Effect.HideWriteBle -> {
+ sheetPage = null
+ delay(100)
+ }
+ is ThermometerContract.Effect.ShowWriteBle -> {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.WRITE
+ }
}
}.launchIn(this)
@@ -118,438 +212,4 @@ fun ThermometerScreen(
}
- sheetPage?.let {
-
- Column() {
-
- ModalBottomSheet(
- modifier = Modifier,
- sheetState = bottomSheetState,
- onDismissRequest = {
- sheetPage = null
- },
- content = {
-
- Column() {
-
- if (state is ThermometerContract.State.Display) {
-
- when (sheetPage) {
- SheetPage.INTERVAL -> {
- IntervalEdit(
- state = state.thermometer,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
- }
- SheetPage.POWER -> {
- PowerEdit(
- state = state.thermometer,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
- }
- SheetPage.TEMPERATURE_HISTORY -> TemperatureHistory(state.thermometer.info)
- null -> {}
- }
-
- }
-
- Spacer(modifier = Modifier.height(48.dp))
-
- }
-
- }
-
- )
-
- }
-
- }
-
- if(state is ThermometerContract.State.Display){
-
- state.writeState?.let {
-
- val scope = rememberCoroutineScope()
-
- ModalBottomSheet(
- modifier = Modifier,
- containerColor = MaterialTheme.colorScheme.surface,
- sheetState = writeSheetState,
- onDismissRequest = {
- viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview)
- },
- content = {
-
- Column() {
-
- when (it) {
- is ThermometerContract.State.Display.WriteState.DisplayPreview -> {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Записать изменения?",
- style = MaterialTheme.typography.titleLarge
- )
-
- it.writeRequest.tx?.let {
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Мощность"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "${state.origin.state.tx.localizedName} db -> ${it.localizedName} db"
- )
-
- }
-
- }
-
- }
- }
-
- it.writeRequest.saveHistory?.let {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Сохранять историю измерений"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "${state.origin.thermometerState.saveHistory.localizedName} -> ${it.localizedName}"
- )
-
- }
-
- }
-
- }
-
- }
-
- it.writeRequest.historyInterval?.let {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Интервал измерний"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "${ state.origin.thermometerState.historyInterval / 1000 / 60 / 60 } ч. -> ${it / 1000 / 60 / 60} ч."
- )
-
- }
-
- }
-
- }
-
- }
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .height(50.dp),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.primaryContainer,
- onClick = {
- viewModel.setEvent(ThermometerContract.Event.OnWriteBle)
- }
- ) {
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- Text(
- modifier = Modifier.align(Alignment.Center),
- color = MaterialTheme.colorScheme.background,
- style = MaterialTheme.typography.labelLarge,
- text = "Записать"
- )
-
- }
-
- }
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .height(50.dp),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.surfaceVariant,
- onClick = {
- scope.launch {
- writeSheetState.hide()
- viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview)
- }
- }
- ) {
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- Text(
- modifier = Modifier.align(Alignment.Center),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.labelLarge,
- text = "Отменить"
- )
-
- }
-
- }
-
-
- }
- is ThermometerContract.State.Display.WriteState.Writing -> {
-
- Box {
-
- Column() {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Запись",
- style = MaterialTheme.typography.titleLarge
- )
-
- Column(
- modifier = Modifier.alpha(0.6f)
- ) {
-
- it.writeRequest.tx?.let {
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Мощность"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "${it} db"
- )
-
- }
-
- }
-
- }
- }
-
- it.writeRequest.saveHistory?.let {
-
- }
-
- it.writeRequest.historyInterval?.let {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Интервал измерний"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "${state.origin.thermometerState.historyInterval / 1000 / 60 / 60} ч. -> ${it / 1000 / 60 / 60} ч."
- )
-
- }
-
- }
-
- }
-
- }
-
- }
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .height(50.dp),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.surfaceVariant,
- onClick = {
- scope.launch {
- writeSheetState.hide()
- viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview)
- }
- }
- ) {
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- Text(
- modifier = Modifier.align(Alignment.Center),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.labelLarge,
- text = "Отменить"
- )
-
- }
-
- }
-
- }
-
- CircularProgressIndicator(
- modifier = Modifier
- .align(Alignment.Center)
- .padding(bottom = 48.dp)
- )
-
- }
-
- }
- ThermometerContract.State.Display.WriteState.Success -> {
-
- Box {
-
- Column {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Запись завершена",
- style = MaterialTheme.typography.titleLarge
- )
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .height(50.dp),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.primary,
- onClick = {
- scope.launch {
- writeSheetState.hide()
- viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview)
- }
- }
- ) {
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- Text(
- modifier = Modifier.align(Alignment.Center),
- color = MaterialTheme.colorScheme.onPrimary,
- style = MaterialTheme.typography.labelLarge,
- text = "Ок"
- )
-
- }
-
- }
-
- }
-
- }
-
- }
-
- }
-
- Spacer(modifier = Modifier.height(48.dp))
-
- }
-
- }
- )
-
- }
-
- }
-
-
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt
index dd404cb..1ed2411 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt
@@ -35,6 +35,7 @@ class ThermometerViewModel @Inject constructor(
is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
+ is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
}
}
@@ -172,6 +173,10 @@ class ThermometerViewModel @Inject constructor(
)
}
+ setEffect {
+ ThermometerContract.Effect.ShowWriteBle
+ }
+
}
}
@@ -195,13 +200,22 @@ class ThermometerViewModel @Inject constructor(
)
}
- writeBle(state.thermometer.info.serial, it.writeRequest)
-
- setState {
- state.copy(
- writeState = ThermometerContract.State.Display.WriteState.Success
- )
- }
+ writeBle(state.thermometer.info.serial, it.writeRequest).fold(
+ onSuccess = {
+ setState {
+ state.copy(
+ writeState = ThermometerContract.State.Display.WriteState.Success
+ )
+ }
+ },
+ onFailure = {
+ setState {
+ state.copy(
+ writeState = ThermometerContract.State.Display.WriteState.Failure
+ )
+ }
+ }
+ )
}
@@ -228,6 +242,25 @@ class ThermometerViewModel @Inject constructor(
}
+ setEffect {
+ ThermometerContract.Effect.HideWriteBle
+ }
+
+ }
+
+ private fun reduce(
+ state: ThermometerContract.State,
+ event: ThermometerContract.Event.OnChangePassword
+ ) {
+
+ if(state is ThermometerContract.State.Display){
+
+ setEffect {
+ ThermometerContract.Effect.Navigation.NavigateToChangePassword
+ }
+
+ }
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt
index 96f3502..08df045 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt
@@ -236,6 +236,42 @@ fun DisplayState(
}
+ Box(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .clickable {
+ onEvent(ThermometerContract.Event.OnChangePassword)
+ }
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Изменить пароль"
+ )
+
+ }
+
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+
+ }
+
+ }
+
}
)
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt
index 7abe4cd..263e297 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt
@@ -11,18 +11,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
-import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
-import com.patrykandpatrick.vico.core.chart.composed.plus
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
-import com.patrykandpatrick.vico.core.entry.FloatEntry
-import com.patrykandpatrick.vico.core.entry.composed.plus
-import com.patrykandpatrick.vico.core.entry.entriesOf
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
@@ -32,8 +26,6 @@ import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
import javax.inject.Inject
-import kotlin.random.Random
-import kotlin.random.nextInt
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.graphics.StrokeCap
@@ -41,7 +33,6 @@ import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
-import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.ChartEntry
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -70,26 +61,39 @@ fun TemperatureHistory(
val viewModel = hiltViewModel()
val state = viewModel.viewState.value
- LaunchedEffect(ble.serial) {
- viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial))
+ LaunchedEffect("ble.serial") {
+ viewModel.setEvent(TemperatureHistoryContract.Event.OnStart(ble.serial))
}
- Column {
+ Column(
+ modifier = Modifier.fillMaxHeight(0.9f)
+ ) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
+ val title = when(state){
+ is TemperatureHistoryContract.State.Display -> {
+ when (state.loadingHistoryState) {
+ is ProgressState.Finished -> "История измерений (${state.loadingHistoryState.data.size})"
+ is ProgressState.Indeterminate -> "История измерений"
+ is ProgressState.Progress -> "История измерений"
+ }
+ }
+ TemperatureHistoryContract.State.Exception -> "История измерений"
+ }
+
Text(
modifier = Modifier.weight(1f),
- text = "История измерений",
+ text = title,
style = MaterialTheme.typography.titleLarge
)
IconButton(
onClick = {
- viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial))
+ viewModel.setEvent(TemperatureHistoryContract.Event.OnRefreshHistory(ble.serial))
},
enabled = when(state){
is TemperatureHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
@@ -106,95 +110,90 @@ fun TemperatureHistory(
Spacer(modifier = Modifier.height(16.dp))
- when(state){
- is TemperatureHistoryContract.State.Display -> Display(state = state)
- TemperatureHistoryContract.State.Exception -> Exception()
+ Box(modifier = Modifier) {
+
+ when (state) {
+ is TemperatureHistoryContract.State.Display -> Display(state = state)
+ TemperatureHistoryContract.State.Exception -> Exception()
+ }
+
}
}
}
+
@Composable
fun Display(
state: TemperatureHistoryContract.State.Display
) {
- when (state.loadingHistoryState) {
- is ProgressState.Finished -> {
- Text(text = "${state.loadingHistoryState.data.size}")
+ Box(modifier = Modifier
+ .padding(8.dp)
+ .fillMaxSize()
+ ) {
- val producer = state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
- TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let {
- ChartEntryModelProducer(it)
- }
+ when (state.loadingHistoryState) {
- val axisValueFormatter = AxisValueFormatter { value, chartValues ->
- (chartValues.chartEntryModel.entries.first().getOrNull(value.toInt()) as? TemperatureEntry)
- ?.localDate
- ?.let { formatter.format(Date(it)) }
- .orEmpty()
- }
+ is ProgressState.Finished -> {
- val lineChart = lineChart(
- spacing = 110.dp
- )
+ val producer = remember(state.loadingHistoryState.data) {
+ state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
+ TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
+ }.let {
+ ChartEntryModelProducer(it)
+ }
+ }
- Box(modifier = Modifier.padding(8.dp)) {
+ val axisValueFormatter =
+ AxisValueFormatter { value, chartValues ->
+ (chartValues.chartEntryModel.entries.first()
+ .getOrNull(value.toInt()) as? TemperatureEntry)
+ ?.localDate
+ ?.let { formatter.format(Date(it)) }
+ .orEmpty()
+ }
+
+ val lineChart = lineChart()
val scrollState = rememberChartScrollState()
- LaunchedEffect(scrollState.maxValue){
- scrollState.scrollBy(scrollState.maxValue)
- }
-
Chart(
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
+ tickLength = 0.dp,
valueFormatter = axisValueFormatter,
- labelRotationDegrees = 0f,
+ labelRotationDegrees = -90f,
),
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(1.5f),
+ modifier = Modifier.fillMaxSize(),
)
- }
- }
- is ProgressState.Indeterminate -> {
-
- Box(modifier = Modifier.padding(8.dp)) {
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(2f),
- ){
-
- CircularProgressIndicator(
- strokeCap = StrokeCap.Round,
- modifier = Modifier.align(Alignment.Center)
- )
-
+ LaunchedEffect(scrollState.maxValue) {
+ scrollState.scrollBy(scrollState.maxValue)
}
}
- }
- is ProgressState.Progress -> Box(modifier = Modifier.padding(8.dp)) {
+ is ProgressState.Indeterminate -> {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(2f),
- ){
+ CircularProgressIndicator(
+ strokeCap = StrokeCap.Round,
+ modifier = Modifier.align(Alignment.Center)
+ )
+
+ }
+ is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
- animationSpec = tween(durationMillis = progressAnimDuration, easing = FastOutSlowInEasing)
+ animationSpec = tween(
+ durationMillis = progressAnimDuration,
+ easing = FastOutSlowInEasing
+ )
)
CircularProgressIndicator(
@@ -206,6 +205,7 @@ fun Display(
}
}
+
}
}
@@ -234,7 +234,11 @@ class TemperatureHistoryContract {
sealed class Event : ViewEvent {
- data class LoadHistory(
+ data class OnStart(
+ val serial: String
+ ) : Event()
+
+ data class OnRefreshHistory(
val serial: String
) : Event()
@@ -269,13 +273,50 @@ class TemperatureHistoryViewModel @Inject constructor(
override fun handleEvents(event: TemperatureHistoryContract.Event) {
when(event){
- is TemperatureHistoryContract.Event.LoadHistory -> reduce(viewState.value, event)
+ is TemperatureHistoryContract.Event.OnStart -> reduce(viewState.value, event)
+ is TemperatureHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
}
}
private fun reduce(
state: TemperatureHistoryContract.State,
- event: TemperatureHistoryContract.Event.LoadHistory
+ event: TemperatureHistoryContract.Event.OnStart
+ ) {
+ viewModelScope.launch {
+
+ if(state is TemperatureHistoryContract.State.Display) {
+
+ if(state.loadingHistoryState is ProgressState.Indeterminate) {
+
+ setState {
+ TemperatureHistoryContract.State.Display(ProgressState.Indeterminate)
+ }
+
+ getTemperatureHistoryBySerial(event.serial).onEach {
+ it.fold(
+ onSuccess = {
+ setState {
+ TemperatureHistoryContract.State.Display(it)
+ }
+ },
+ onFailure = {
+ setState {
+ TemperatureHistoryContract.State.Exception
+ }
+ }
+ )
+ }.launchIn(this)
+
+ }
+
+ }
+
+ }
+ }
+
+ private fun reduce(
+ state: TemperatureHistoryContract.State,
+ event: TemperatureHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt
new file mode 100644
index 0000000..d23b38b
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt
@@ -0,0 +1,381 @@
+package llc.arma.ble.app.ui.screen.thermometer.view
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import llc.arma.ble.R
+import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
+import llc.arma.ble.app.ui.screen.thermometer.localizedName
+
+@Composable
+fun Write(
+ state: ThermometerContract.State.Display.WriteState,
+ onEvent: (ThermometerContract.Event) -> Unit
+) {
+
+ val scope = rememberCoroutineScope()
+
+ Column(
+ modifier = Modifier.animateContentSize { initialValue, targetValue -> }
+ ) {
+
+ Text(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ text = "Запись изменений",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ when (state) {
+ is ThermometerContract.State.Display.WriteState.DisplayPreview -> {
+
+ state.writeRequest.tx?.let {
+ Box(
+ modifier = Modifier.padding(
+ vertical = 0.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Мощность"
+ )
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${it.localizedName} db"
+ )
+
+ }
+
+ }
+
+ }
+ }
+
+ state.writeRequest.saveHistory?.let {
+
+ Box(
+ modifier = Modifier.padding(
+ vertical = 0.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Сохранять историю измерений"
+ )
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${it.localizedName}"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ state.writeRequest.historyInterval?.let {
+
+ Box(
+ modifier = Modifier.padding(
+ vertical = 0.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .padding(8.dp)
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = "Интервал измерний"
+ )
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = "${it / 1000 / 60 / 60} ч."
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onEvent(ThermometerContract.Event.OnWriteBle)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Записать"
+ )
+
+ }
+
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+
+ }
+ is ThermometerContract.State.Display.WriteState.Writing -> {
+
+ Box {
+
+ Column() {
+
+ Spacer(modifier = Modifier.height(28.dp))
+
+ CircularProgressIndicator(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ )
+
+ Spacer(modifier = Modifier.height(48.dp))
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ onClick = {
+ onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ ThermometerContract.State.Display.WriteState.Success -> {
+
+ Box {
+
+ Column {
+
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+
+ Image(
+ modifier = Modifier
+ .size(125.dp)
+ .align(Alignment.Center),
+ painter = painterResource(llc.arma.ble.R.drawable.ic_done),
+ contentDescription = null
+ )
+
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Успешно завершено"
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ onClick = {
+ onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ ThermometerContract.State.Display.WriteState.Failure -> {
+
+ Box {
+
+ Column {
+
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+
+ Image(
+ modifier = Modifier
+ .size(125.dp)
+ .align(Alignment.Center),
+ painter = painterResource(R.drawable.ic_error),
+ contentDescription = null
+ )
+
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Ошибка записи"
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ onClick = {
+ onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt
index c0a5a86..71340fd 100644
--- a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt
+++ b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt
@@ -23,12 +23,24 @@ import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.usecase.GetBleBySerial
+import java.nio.charset.Charset
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
+val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")
+
+val temperatureHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
+val temperatureReadUUID: UUID = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
+val intervalReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
+val intervalWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+val saveEnabledWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+val passwordWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+val txWriteUUID: UUID = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb")
+val flashWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+
@Singleton
class BleRepositoryImpl @Inject constructor(
private val app: Application
@@ -75,15 +87,12 @@ class BleRepositoryImpl @Inject constructor(
override fun getBleAroundFlow(): Flow, BleException>> {
- return if (ActivityCompat.checkSelfPermission(
+ return if(
+ Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_SCAN
- ) != PackageManager.PERMISSION_GRANTED
- ) {
-
- flow { emit(Result.failure(BleException.PermissionDenied)) }
-
- } else {
+ ) == PackageManager.PERMISSION_GRANTED
+ ){
callbackFlow {
@@ -96,7 +105,7 @@ class BleRepositoryImpl @Inject constructor(
super.onScanResult(callbackType, result)
- if (ActivityCompat.checkSelfPermission(
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
@@ -143,7 +152,7 @@ class BleRepositoryImpl @Inject constructor(
send(Result.success(resultList.values.toList()))
}
}
- }, 100, 500)
+ }, 500, 500)
}
awaitClose {
@@ -153,6 +162,10 @@ class BleRepositoryImpl @Inject constructor(
}
+ } else {
+
+ flow { emit(Result.failure(BleException.PermissionDenied)) }
+
}
}
@@ -164,7 +177,7 @@ class BleRepositoryImpl @Inject constructor(
deviceCache[serial]?.let { result ->
- if (ActivityCompat.checkSelfPermission(
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
@@ -264,8 +277,8 @@ class BleRepositoryImpl @Inject constructor(
val dataResult = readCharacteristic(
device = record.device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
+ serviceId = serviceUUID,
+ characteristicId = temperatureReadUUID
).fold(
onFailure = {
return Result.failure(it)
@@ -283,15 +296,17 @@ class BleRepositoryImpl @Inject constructor(
writeCharacteristic(
device = record.device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
+ serviceId = serviceUUID,
+ characteristicId = intervalReadUUID,
writeData = byteArrayOf(3, 0, 0, 0, 0)
- )
+ ).onFailure {
+ return Result.failure(it)
+ }
val dataResult = readCharacteristic(
device = record.device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
+ serviceId = serviceUUID,
+ characteristicId = intervalReadUUID
).fold(
onFailure = {
return Result.failure(it)
@@ -311,221 +326,153 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun getTemperatureHistoryBySerial(
serial: String
- ): Flow>, BleException>> = flow {
+ ): Flow>, BleException>> {
- fun ByteArray.getUIntAt(idx: Int) =
- ((this[idx + 3].toUInt() and 0xFFu) shl 24) or
- ((this[idx + 2].toUInt() and 0xFFu) shl 16) or
- ((this[idx + 1].toUInt() and 0xFFu) shl 8) or
- (this[idx].toUInt() and 0xFFu)
+ var gatt: BluetoothGatt? = null
- findDeviceBySerial(serial).fold(
- onSuccess = {
- return@fold it
- },
- onFailure = {
- emit(Result.failure(it))
- return@flow
- }
- ).let { device ->
+ return callbackFlow {
- emit(Result.success(ProgressState.Indeterminate))
+ deviceCache[serial]?.device?.let {
- writeCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
- writeData = byteArrayOf(2)
- )
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
- val countDataArray = readCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
- ).fold(
- onFailure = {
- emit(Result.failure(it))
- return@flow
- },
- onSuccess = { return@fold it }
- )
+ gatt = it.connectGatt(app, false, ReadHistoryCallback(app) {
+ CoroutineScope(Dispatchers.IO).launch {
+ send(it)
+ }
+ })
- writeCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
- writeData = mutableListOf(
- 1.toByte(),
- 0.toByte(),
- 0.toByte()
- ).apply {
- addAll(countDataArray.toList())
- }.toByteArray()
- )
-
- val firstPackageResponse = readCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
- ).fold(
- onFailure = {
- emit(Result.failure(it))
- return@flow
- },
- onSuccess = { return@fold it }
- )
-
- if(firstPackageResponse[0] == 250.toByte()){
-
- val interval = firstPackageResponse.getUIntAt(2).toLong()
- val lastMeasureTime = firstPackageResponse.getUIntAt(6).toLong()
- val realTime = firstPackageResponse.getUIntAt(10).toLong()
-
- val lastMeasureSystemTime = System.currentTimeMillis() - ((realTime - lastMeasureTime) / 10_000)
-
- var temperatureDataArray = firstPackageResponse.asList().subList(14, firstPackageResponse.size)
-
- val temperaturePackage = temperatureDataArray.chunked(2).map {
- (it[0] + it[1] * 256).toFloat() / 100f
- }.toMutableList()
-
- var dataCount = firstPackageResponse[1].toUByte()
- val totalDataSize = dataCount.toInt() + temperaturePackage.size
-
- emit(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
- delay(100)
- emit(Result.success(ProgressState.Progress(dataCount.toFloat() / totalDataSize.toFloat())))
-
- while(dataCount != 0.toUByte()){
-
- writeCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
- writeData = byteArrayOf(5)
- )
-
- val readResponse = readCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
- ).fold(
- onFailure = {
- emit(Result.failure(it))
- return@flow
- },
- onSuccess = { return@fold it }
- )
-
- if(readResponse[0] == 251.toByte()) {
-
- dataCount = readResponse[1].toUByte()
-
- temperatureDataArray = readResponse.toList().subList(2, readResponse.size)
-
- temperaturePackage.addAll(
- temperatureDataArray.chunked(2).map {
- (it[0] + it[1] * 256).toFloat() / 100f
- }
- )
-
- emit(Result.success(ProgressState.Progress(totalDataSize.toFloat() / temperaturePackage.size.toFloat())))
-
- } else {
-
- emit(Result.failure(BleException.UnexpectedResponse))
+ } else {
+ CoroutineScope(Dispatchers.IO).launch {
+ send(Result.failure(BleException.PermissionDenied))
}
+ return@callbackFlow
+
}
- readCharacteristic(
- device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
- )
-
- emit(
- Result.success(
- ProgressState.Finished(
- temperaturePackage.withIndex().map {
- Ble.Thermometer.MeasurePoint(
- date = lastMeasureSystemTime - (((temperaturePackage.size - 1) - it.index) * interval),
- value = it.value
- )
- }
- )
- )
- )
-
- } else {
-
- emit(Result.failure(BleException.UnexpectedResponse))
-
}
- }
+ awaitClose {
+ gatt?.close()
+ }
- }
-
- override suspend fun writeBle(ble: Ble) {
- when(ble){
- is Ble.Beacon -> writeBeacon(ble)
- is Ble.Thermometer -> writeThermometer(ble)
}
}
override suspend fun writeBle(
serial: String,
request: Ble.Thermometer.WriteRequest
- ) {
+ ): Result {
deviceCache[serial]?.let { result ->
- request.tx?.let { writeTx(result.device, it) }
+ request.tx?.let { writeTx(result.device, it) }?.onFailure {
+ return Result.failure(it)
+ }
- request.historyInterval?.let { writeSaveInterval(result.device, it) }
+ request.historyInterval?.let { writeSaveInterval(result.device, it) }?.onFailure {
+ return Result.failure(it)
+ }
- request.saveHistory?.let { writeSaveEnabled(result.device, it) }
+ request.saveHistory?.let { writeSaveEnabled(result.device, it) }?.onFailure {
+ return Result.failure(it)
+ }
+
+ writeToFlash(serial).onFailure {
+ return Result.failure(it)
+ }
deviceCache.remove(serial)
resultList.remove(serial)
}
- }
-
- private suspend fun writeBeacon(ble: Ble.Beacon){
-
- deviceCache[ble.info.serial]?.device?.let {
-
- writeTx(it, ble.state.tx)
-
- }
+ return Result.success(Unit)
}
- private suspend fun writeThermometer(ble: Ble.Thermometer){
+ override suspend fun writeBle(
+ serial: String,
+ request: Ble.Beacon.WriteRequest
+ ): Result {
- deviceCache[ble.info.serial]?.device?.let {
+ deviceCache[serial]?.let { result ->
- writeTx(it, ble.state.tx)
+ request.tx?.let { writeTx(result.device, it) }?.onFailure {
+ return Result.failure(it)
+ }
- writeSaveInterval(it, ble.thermometerState.historyInterval)
+ writeToFlash(serial).onFailure {
+ return Result.failure(it)
+ }
+
+ deviceCache.remove(serial)
+ resultList.remove(serial)
}
+ return Result.success(Unit)
+
+ }
+
+ private suspend fun writeToFlash(
+ serial: String
+ ): Result{
+
+ deviceCache[serial]?.device?.let { result ->
+
+ return writeCharacteristic(
+ device = result,
+ serviceId = serviceUUID,
+ characteristicId = flashWriteUUID,
+ writeData = byteArrayOf(9, 1)
+ )
+
+ }
+
+ return Result.success(Unit)
+
+ }
+
+ override suspend fun changeBlePassword(
+ password: String,
+ serial: String
+ ): Result {
+ deviceCache[serial]?.device?.let {
+ return writeCharacteristic(
+ device = it,
+ serviceId = serviceUUID,
+ characteristicId = passwordWriteUUID,
+ writeData = mutableListOf(8.toByte()).apply {
+ addAll(password.toByteArray(Charsets.US_ASCII).toList())
+ }.toByteArray()
+ ).fold(
+ onFailure = {
+ Result.failure(it)
+ },
+ onSuccess = {
+ Result.success(Unit)
+ }
+ )
+ }
+ return Result.success(Unit)
}
private suspend fun writeTx(
device: BluetoothDevice,
tx: Ble.BleState.TX
- ) {
+ ): Result {
- writeCharacteristic(
+ return writeCharacteristic(
device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb"),
+ serviceId = serviceUUID,
+ characteristicId = txWriteUUID,
writeData = byteArrayOf(
when(tx) {
Ble.BleState.TX.MINUS_40 -> -40
@@ -546,17 +493,17 @@ class BleRepositoryImpl @Inject constructor(
private suspend fun writeSaveInterval(
device: BluetoothDevice,
interval: Long
- ) {
+ ): Result {
fun UInt.to4ByteArrayInBigEndian(): ByteArray =
(3 downTo 0).map {
(this shr (it * Byte.SIZE_BITS)).toByte()
}.reversed().toByteArray()
- writeCharacteristic(
+ return writeCharacteristic(
device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"),
+ serviceId = serviceUUID,
+ characteristicId = intervalWriteUUID,
writeData = mutableListOf(3).apply {
addAll(interval.toUInt().to4ByteArrayInBigEndian().toList())
}.toByteArray()
@@ -569,17 +516,15 @@ class BleRepositoryImpl @Inject constructor(
enabled: Boolean
): Result {
- writeCharacteristic(
+ return writeCharacteristic(
device = device,
- serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
- characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"),
+ serviceId = serviceUUID,
+ characteristicId = saveEnabledWriteUUID,
writeData = mutableListOf(4).apply {
add(if(enabled) 1 else 0)
}.toByteArray()
)
- return Result.success(Unit)
-
}
private suspend fun readCharacteristic(
@@ -601,18 +546,30 @@ class BleRepositoryImpl @Inject constructor(
Log.d("read", "onConnectionStateChange $newState $status")
- if (newState == BluetoothProfile.STATE_CONNECTED) {
+ if(status == BluetoothGatt.GATT_SUCCESS) {
+
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+
+ gatt.discoverServices()
+
+ } else {
+
+ it.resume(Result.failure(BleException.PermissionDenied))
+
+ }
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
- app,
- Manifest.permission.BLUETOOTH_CONNECT
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- gatt.discoverServices()
- } else {
- it.resume(Result.failure(BleException.PermissionDenied))
}
+ } else {
+
+ it.resume(Result.failure(BleException.PermissionDenied))
+
}
}
@@ -633,22 +590,52 @@ class BleRepositoryImpl @Inject constructor(
characteristic.uuid == characteristicId
}?.let { char ->
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
+
gatt.readCharacteristic(char)
+
} else {
+
it.resume(Result.failure(BleException.PermissionDenied))
+
}
+ return
+
}
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+
+ }else{
+ it.resume(Result.failure(BleException.UnexpectedResponse))
}
}
+ @Deprecated("Deprecated in Java")
+ override fun onCharacteristicRead(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ status: Int
+ ) {
+ super.onCharacteristicRead(gatt, characteristic, status)
+
+ result = characteristic.value
+ if (result != null) {
+ it.resume(Result.success(result!!))
+ } else {
+ bleGatt?.close()
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+ }
+
+ gatt.close()
+
+ }
+
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
@@ -659,36 +646,40 @@ class BleRepositoryImpl @Inject constructor(
Log.d("read", "onCharacteristicRead $status")
- if (ActivityCompat.checkSelfPermission(
- app,
- Manifest.permission.BLUETOOTH_CONNECT
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- it.resume(Result.failure(BleException.PermissionDenied))
- }else {
- gatt.close()
- result = value
- if(result != null){
+ if(status == BluetoothGatt.GATT_SUCCESS) {
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+
+ gatt.close()
+ result = value
it.resume(Result.success(result!!))
+
} else {
- bleGatt?.close()
- it.resume(Result.failure(BleException.UnexpectedResponse))
+ it.resume(Result.failure(BleException.PermissionDenied))
+
}
+ } else {
+
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+
}
}
}
- if (ActivityCompat.checkSelfPermission(
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- it.resume(Result.failure(BleException.PermissionDenied))
+ ) == PackageManager.PERMISSION_GRANTED) {
+ bleGatt = device.connectGatt(app, false, callback)
} else {
- bleGatt = device.connectGatt(app, true, callback)
+ it.resume(Result.failure(BleException.PermissionDenied))
}
}
@@ -698,7 +689,7 @@ class BleRepositoryImpl @Inject constructor(
serviceId: UUID,
characteristicId: UUID,
writeData: ByteArray
- ) = suspendCancellableCoroutine {
+ ): Result = suspendCancellableCoroutine {
var bleGatt: BluetoothGatt? = null
@@ -710,23 +701,35 @@ class BleRepositoryImpl @Inject constructor(
newState: Int
) {
- Log.d("write", "onConnectionStateChange $newState")
+ Log.d("write", "onConnectionStateChange $status $newState")
- if (newState == BluetoothProfile.STATE_CONNECTED) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ gatt.discoverServices()
+ } else {
+
+ it.resume(Result.failure(BleException.PermissionDenied))
+
+ }
+
+ } else {
+
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+ bleGatt?.close()
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
- app,
- Manifest.permission.BLUETOOTH_CONNECT
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- gatt.discoverServices()
}
} else {
- if(newState == BluetoothProfile.STATE_DISCONNECTED && status == BluetoothGatt.GATT_FAILURE){
- bleGatt?.close()
- }
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+ bleGatt?.close()
}
@@ -737,7 +740,9 @@ class BleRepositoryImpl @Inject constructor(
status: Int
) {
super.onServicesDiscovered(gatt, status)
+
Log.d("write", "onServicesDiscovered $status")
+
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.services?.firstOrNull { service ->
@@ -746,23 +751,30 @@ class BleRepositoryImpl @Inject constructor(
characteristic.uuid == characteristicId
}?.let { char ->
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- gatt.writeCharacteristic(char, writeData, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
- }else{
- char.value = writeData
- gatt.writeCharacteristic(char)
- }
+ gatt.writeCharacteristic(char, writeData)
+
+ } else {
+
+ it.resume(Result.failure(BleException.PermissionDenied))
}
+ return
+
}
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+
+ } else {
+
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+
}
}
@@ -776,79 +788,61 @@ class BleRepositoryImpl @Inject constructor(
Log.d("write", "onCharacteristicWrite $status")
- if (ActivityCompat.checkSelfPermission(
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
- ) != PackageManager.PERMISSION_GRANTED
+ ) == PackageManager.PERMISSION_GRANTED
) {
- return
- } else {
+
gatt.close()
- it.resume(Unit)
+
+ if(status == BluetoothGatt.GATT_SUCCESS) {
+
+ it.resume(Result.success(Unit))
+
+ }else{
+
+ it.resume(Result.failure(BleException.UnexpectedResponse))
+
+ }
+
+ } else {
+
+ it.resume(Result.failure(BleException.PermissionDenied))
+
}
}
}
- bleGatt = device.connectGatt(app, true, callback)
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED) {
+
+ bleGatt = device.connectGatt(app, false, callback)
+
+ } else {
+
+ it.resume(Result.failure(BleException.PermissionDenied))
+
+ }
}
- private suspend fun findDeviceBySerial(serial: String): Result = suspendCancellableCoroutine {
+}
- val bleCallback = object : ScanCallback() {
-
- override fun onScanResult(
- callbackType: Int,
- result: ScanResult
- ) {
-
- super.onScanResult(callbackType, result)
-
- if(it.isActive) {
-
- if (ActivityCompat.checkSelfPermission(
- app,
- Manifest.permission.BLUETOOTH_CONNECT
- ) == PackageManager.PERMISSION_GRANTED
- ) {
-
-
- it.resume(Result.success(result.device))
-
- } else {
- CoroutineScope(Dispatchers.IO).launch {
- it.resume(
- Result.failure(BleException.PermissionDenied)
- )
- }
- }
-
- }
-
- }
-
- }
-
- val bleScanner =
- app.getSystemService(BluetoothManager::class.java).adapter.bluetoothLeScanner
-
- bleScanner.startScan(
- listOf(ScanFilter.Builder().setDeviceAddress(serial).build()),
- ScanSettings.Builder()
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
- .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
- .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
- .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
- .setReportDelay(400L)
- .build(),
- bleCallback)
-
- it.invokeOnCancellation {
- bleScanner.stopScan(bleCallback)
- }
+fun BluetoothGatt.writeCharacteristic(
+ characteristic: BluetoothGattCharacteristic,
+ data: ByteArray
+){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
+ }else{
+ characteristic.value = data
+ writeCharacteristic(characteristic)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt b/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt
new file mode 100644
index 0000000..ed10025
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt
@@ -0,0 +1,292 @@
+package llc.arma.ble.data
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Application
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCallback
+import android.bluetooth.BluetoothGattCharacteristic
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.core.app.ActivityCompat
+import llc.arma.ble.domain.Result
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+import java.util.*
+
+enum class Property {
+ DATA_SIZE, PACKAGE
+}
+
+class ReadHistoryCallback(
+ private val app: Application,
+ private val onResult: (Result>, BleException>) -> Unit
+) : BluetoothGattCallback() {
+
+ private fun ByteArray.getUIntAt(idx: Int) =
+ ((this[idx + 3].toUInt() and 0xFFu) shl 24) or
+ ((this[idx + 2].toUInt() and 0xFFu) shl 16) or
+ ((this[idx + 1].toUInt() and 0xFFu) shl 8) or
+ (this[idx].toUInt() and 0xFFu)
+
+ private var readProperty: Property? = null
+
+ init {
+ onResult(Result.success(ProgressState.Indeterminate))
+ }
+
+ override fun onConnectionStateChange(
+ gatt: BluetoothGatt,
+ status: Int,
+ newState: Int
+ ) {
+ super.onConnectionStateChange(gatt, status, newState)
+
+ if(status == BluetoothGatt.GATT_SUCCESS){
+
+ if(newState == BluetoothGatt.STATE_CONNECTED){
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ gatt.discoverServices()
+
+ } else {
+ onResult(Result.failure(BleException.UnexpectedResponse))
+ gatt.close()
+ }
+ }
+
+ } else {
+
+ onResult(Result.failure(BleException.UnexpectedResponse))
+ gatt.close()
+
+ }
+
+ }
+
+ override fun onServicesDiscovered(
+ gatt: BluetoothGatt,
+ status: Int
+ ) {
+ super.onServicesDiscovered(gatt, status)
+ if(status == BluetoothGatt.GATT_SUCCESS){
+ gatt.getService(serviceUUID)?.getCharacteristic(temperatureHistoryReadUUID)?.let {
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+
+ readProperty = Property.DATA_SIZE
+ gatt.writeCharacteristic(it, byteArrayOf(2))
+
+ } else {
+
+ onResult(Result.failure(BleException.PermissionDenied))
+ gatt.close()
+
+ }
+
+ }
+
+ }
+
+ }
+
+ private var lastMeasureSystemTime: Long? = null
+
+ private var bleMeasureInterval: Long? = null
+ private var bleRealTime: Long? = null
+ private var bleLastMeasureTime: Long? = null
+
+ private val resultTemperaturePackage: MutableList = mutableListOf()
+
+ var expectedDataSize: Int? = null
+
+ override fun onCharacteristicRead(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ status: Int
+ ) {
+ super.onCharacteristicRead(gatt, characteristic, status)
+ onCommonCharacteristicRead(gatt, characteristic, characteristic.value, status)
+ }
+
+ override fun onCharacteristicRead(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ value: ByteArray,
+ status: Int
+ ) {
+ super.onCharacteristicRead(gatt, characteristic, value, status)
+ //onCommonCharacteristicRead(gatt, characteristic, value, status)
+ }
+
+ private fun onCommonCharacteristicRead(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ value: ByteArray,
+ status: Int
+ ){
+ if(status == BluetoothGatt.GATT_SUCCESS){
+ when(readProperty){
+ Property.DATA_SIZE -> {
+ val writeData = mutableListOf(
+ 1.toByte(),
+ 0.toByte(),
+ 0.toByte()
+ ).apply {
+ addAll(value.toList())
+ }.toByteArray()
+
+ readProperty = Property.PACKAGE
+ gatt.writeCharacteristic(characteristic, writeData)
+ }
+ Property.PACKAGE -> {
+
+ if(value[0] == 250.toByte()){
+
+ bleMeasureInterval = value.getUIntAt(2).toLong()
+ bleLastMeasureTime = value.getUIntAt(6).toLong()
+ bleRealTime = value.getUIntAt(10).toLong()
+
+ lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) / 10_000)
+
+ val temperatureDataArray = value.asList().subList(14, value.size)
+
+ resultTemperaturePackage.addAll(
+ temperatureDataArray.chunked(2).map {
+ (it[0] + it[1] * 256).toFloat() / 100f
+ }.toMutableList()
+ )
+
+ val totalDataSize = value[1].toUByte().toInt() + temperatureDataArray.size / 2
+
+ val nextPackageDataCount = value[1].toUByte()
+ expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
+
+ onResult(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
+ onResult(Result.success(ProgressState.Progress(nextPackageDataCount.toFloat() / totalDataSize.toFloat())))
+
+ if(nextPackageDataCount != 0.toUByte()){
+
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+
+ gatt.writeCharacteristic(characteristic, byteArrayOf(5))
+ gatt.readCharacteristic(characteristic)
+
+ } else {
+
+ onResult(Result.failure(BleException.PermissionDenied))
+ gatt.close()
+
+ }
+
+ } else {
+ onResult(
+ Result.success(
+ ProgressState.Finished(
+ resultTemperaturePackage.withIndex().map {
+ Ble.Thermometer.MeasurePoint(
+ date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
+ value = it.value
+ )
+ }
+ )
+ )
+ )
+ gatt.close()
+ }
+
+ }
+
+ if(value[0] == 251.toByte()) {
+
+ val nextPackageDataCount = value[1].toUByte()
+ val temperatureDataArray = value.toList().subList(2, value.size)
+
+ resultTemperaturePackage.addAll(
+ temperatureDataArray.chunked(2).map {
+ (it[0] + it[1] * 256).toFloat() / 100f
+ }
+ )
+
+ onResult(Result.success(ProgressState.Progress(expectedDataSize!!.toFloat() / resultTemperaturePackage.size.toFloat())))
+
+ if(nextPackageDataCount != 0.toUByte()){
+
+ val writeData = byteArrayOf(5)
+
+ gatt.writeCharacteristic(characteristic, writeData)
+ gatt.readCharacteristic(characteristic)
+
+ } else {
+ onResult(
+ Result.success(
+ ProgressState.Finished(
+ resultTemperaturePackage.withIndex().map {
+ Ble.Thermometer.MeasurePoint(
+ date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
+ value = it.value
+ )
+ }
+ )
+ )
+ )
+ gatt.close()
+ }
+ }
+ }
+ else -> {
+ onResult(Result.failure(BleException.UnexpectedResponse))
+ gatt.close()
+
+ }
+
+ }
+
+ }
+ }
+
+ override fun onCharacteristicWrite(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ status: Int
+ ) {
+ super.onCharacteristicWrite(gatt, characteristic, status)
+ if(status == BluetoothGatt.GATT_SUCCESS){
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
+ app,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+
+ gatt.readCharacteristic(characteristic)
+
+ } else {
+
+ onResult(Result.failure(BleException.PermissionDenied))
+ gatt.close()
+
+ }
+
+ } else {
+ onResult(Result.failure(BleException.UnexpectedResponse))
+ gatt.close()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt
index eb24680..38b5ffc 100644
--- a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt
+++ b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt
@@ -16,8 +16,10 @@ interface BleRepository {
suspend fun getTemperatureHistoryBySerial(serial: String): Flow>, BleException>>
- suspend fun writeBle(ble: Ble)
+ suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest): Result
- suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest)
+ suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result
+
+ suspend fun changeBlePassword(password: String, serial: String): Result
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/ChangeBlePassword.kt b/app/src/main/java/llc/arma/ble/domain/usecase/ChangeBlePassword.kt
new file mode 100644
index 0000000..3667fd6
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/ChangeBlePassword.kt
@@ -0,0 +1,18 @@
+package llc.arma.ble.domain.usecase
+
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.repository.BleRepository
+import javax.inject.Inject
+
+class ChangeBlePassword @Inject constructor(
+ private val bleRepository: BleRepository
+) {
+
+ suspend operator fun invoke(
+ password: String,
+ serial: String
+ ): llc.arma.ble.domain.Result {
+ return bleRepository.changeBlePassword(password, serial)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt
index 8e876f3..1bab00b 100644
--- a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt
@@ -1,6 +1,7 @@
package llc.arma.ble.domain.usecase
import android.app.appsearch.SetSchemaRequest
+import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject
@@ -9,15 +10,18 @@ class WriteBle @Inject constructor(
private val bleRepository: BleRepository
) {
- suspend operator fun invoke(ble: Ble){
- bleRepository.writeBle(ble)
+ suspend operator fun invoke(
+ serial: String,
+ request: Ble.Thermometer.WriteRequest
+ ): llc.arma.ble.domain.Result{
+ return bleRepository.writeBle(serial, request)
}
suspend operator fun invoke(
serial: String,
- request: Ble.Thermometer.WriteRequest
- ){
- bleRepository.writeBle(serial, request)
+ request: Ble.Beacon.WriteRequest
+ ): llc.arma.ble.domain.Result{
+ return bleRepository.writeBle(serial, request)
}
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml
new file mode 100644
index 0000000..da4babe
--- /dev/null
+++ b/app/src/main/res/drawable/ic_done.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml
new file mode 100644
index 0000000..731c48b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..bb853a2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index eca70cf..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cf..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..f650714
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..50622ec
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..60a494a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da88191
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..56f1fee
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d1141ec
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..0ef9825
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..daacfd7
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ebe5e94
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1ac9754
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..f7d8445
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d71696a..ea4ad9d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- Ble
+ Arma BLE
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 04e3c67..d75ad32 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,9 +1,18 @@
+
+
+
\ No newline at end of file