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