This commit is contained in:
Vineyro 2023-03-31 16:54:33 +07:00
parent 0d7019a7be
commit 806ceb1fa8
61 changed files with 3414 additions and 1104 deletions

View File

@ -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'

View File

@ -7,6 +7,9 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:maxSdkVersion="30" android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:maxSdkVersion="30" android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
@ -22,14 +25,14 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Ble"
android:theme="@style/Theme.App.Starting"
tools:targetApi="31"
android:name=".app.framework.App">
<activity
android:name=".app.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.Ble">
android:theme="@style/Theme.App.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -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()
}
}
)
}
}

View File

@ -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<BottomState?> { 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 { }
}
}

View File

@ -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()
}

View File

@ -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<BeaconViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(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 = "Сохранить"
)
}
}
}
}

View File

@ -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<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
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
)
}
}
)
}
}
}
}
}
}

View File

@ -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 = "Сохранить"
)
}
}
}
}

View File

@ -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 = "Применить"
)
}
}
}
}

View File

@ -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()
}
}

View File

@ -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 = "Повторить"
)
}
}
}
}

View File

@ -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<ConnectionContract.State, ConnectionContract.Event, ConnectionContract.Effect>() {
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<String>("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<String>("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<String>("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
}
}
}

View File

@ -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()
}
}
}
)
}
)

View File

@ -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()
}
}
}

View File

@ -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<ChangePasswordViewModel>()
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 = "Отменить"
)
}
}
}
}
}
}
}
}

View File

@ -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<ChangePasswordContract.State, ChangePasswordContract.Event, ChangePasswordContract.Effect>() {
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<String>("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
}
}
}

View File

@ -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 = "Отменить"
)
}
}
}
}
}

View File

@ -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 = "Отменить"
)
}
}
}
}
}

View File

@ -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 = "Ок"
)
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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<SheetPage?>(null)
}
val viewModel = hiltViewModel<ThermometerViewModel>()
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))
}
}
)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
)
}
}
}
)

View File

@ -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<TemperatureHistoryViewModel>()
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<AxisPosition.Horizontal.Bottom> { 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<AxisPosition.Horizontal.Bottom> { 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 {

View File

@ -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 = "Ок"
)
}
}
}
}
}
}
}
}

View File

@ -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<Result<List<BleInfo>, 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<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> = flow {
): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, 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<Unit, BleException> {
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<Unit, BleException> {
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<Unit, BleException>{
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<Unit, BleException> {
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<Unit, BleException> {
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<Unit, BleException> {
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<Byte>(3).apply {
addAll(interval.toUInt().to4ByteArrayInBigEndian().toList())
}.toByteArray()
@ -569,17 +516,15 @@ class BleRepositoryImpl @Inject constructor(
enabled: Boolean
): Result<Unit, BleException> {
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<Byte>(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<Unit, BleException> = 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<BluetoothDevice, BleException> = 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)
}
}

View File

@ -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<ProgressState<List<Ble.Thermometer.MeasurePoint>>, 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<Float> = 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()
}
}
}

View File

@ -16,8 +16,10 @@ interface BleRepository {
suspend fun getTemperatureHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>>
suspend fun writeBle(ble: Ble)
suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest): Result<Unit, BleException>
suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest)
suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result<Unit, BleException>
suspend fun changeBlePassword(password: String, serial: String): Result<Unit, BleException>
}

View File

@ -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<Unit, BleException> {
return bleRepository.changeBlePassword(password, serial)
}
}

View File

@ -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<Unit, BleException>{
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<Unit, BleException>{
return bleRepository.writeBle(serial, request)
}
}

View File

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="125dp"
android:height="125dp"
android:viewportWidth="125"
android:viewportHeight="125">
<group>
<clip-path
android:pathData="M0,0h125v125h-125z"/>
<path
android:pathData="M62.5,125C97.02,125 125,97.02 125,62.5C125,27.98 97.02,0 62.5,0C27.98,0 0,27.98 0,62.5C0,97.02 27.98,125 62.5,125Z"
android:fillColor="#32BA7C"/>
<path
android:pathData="M46.53,90.69L78.67,122.83C105.28,115.73 125,91.48 125,62.5V60.73L99.76,37.46L46.53,90.69Z"
android:fillColor="#0AA06E"/>
<path
android:pathData="M64.08,76.5C66.84,79.26 66.84,83.99 64.08,86.75L58.36,92.47C55.6,95.23 50.87,95.23 48.11,92.47L23.07,67.23C20.31,64.47 20.31,59.74 23.07,56.98L28.79,51.26C31.55,48.5 36.28,48.5 39.04,51.26L64.08,76.5Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M85.96,32.93C88.72,30.17 93.45,30.17 96.21,32.93L101.93,38.64C104.69,41.4 104.69,46.14 101.93,48.9L58.56,92.07C55.8,94.83 51.06,94.83 48.3,92.07L42.59,86.36C39.83,83.6 39.83,78.86 42.59,76.1L85.96,32.93Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View File

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="125dp"
android:height="125dp"
android:viewportWidth="125"
android:viewportHeight="125">
<group>
<clip-path
android:pathData="M0,0h125v125h-125z"/>
<path
android:pathData="M62.5,125C97.02,125 125,97.02 125,62.5C125,27.98 97.02,0 62.5,0C27.98,0 0,27.98 0,62.5C0,97.02 27.98,125 62.5,125Z"
android:fillColor="#F15249"/>
<path
android:pathData="M36.28,90.69L69.99,124.41C98.38,121.06 120.86,98.78 124.41,70.39L90.5,36.47L36.28,90.69Z"
android:fillColor="#AD0E0E"/>
<path
android:pathData="M92.07,76.3C94.83,79.06 94.83,83.79 92.07,86.55L86.55,92.07C83.79,94.83 79.06,94.83 76.3,92.07L32.93,48.7C30.17,45.94 30.17,41.21 32.93,38.45L38.64,32.73C41.4,29.97 46.14,29.97 48.9,32.73L92.07,76.3Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M69.2,53.23L53.23,69.2L76.3,92.07C79.06,94.83 83.79,94.83 86.55,92.07L92.27,86.36C95.03,83.6 95.03,78.86 92.27,76.1L69.2,53.23Z"
android:fillColor="#D6D6D6"/>
<path
android:pathData="M76.3,32.93C79.06,30.17 83.79,30.17 86.55,32.93L92.27,38.64C95.03,41.4 95.03,46.14 92.27,48.9L48.7,92.07C45.94,94.83 41.21,94.83 38.45,92.07L32.93,86.55C30.17,83.79 30.17,79.06 32.93,76.3L76.3,32.93Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View File

@ -0,0 +1,84 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="125"
android:viewportHeight="125">
<group android:scaleX="0.385"
android:scaleY="0.385"
android:translateX="38.4375"
android:translateY="38.4375">
<group>
<clip-path
android:pathData="M0,0h125v125h-125z"/>
<path
android:pathData="M122.07,71.29C123.69,71.29 125,69.98 125,68.36C125,66.74 123.69,65.43 122.07,65.43H103.79V59.57H122.07C123.69,59.57 125,58.26 125,56.64C125,55.02 123.69,53.71 122.07,53.71H103.79V47.85H122.07C123.68,47.85 125,46.54 125,44.92C125,43.3 123.68,41.99 122.07,41.99H103.79V36.13H122.07C123.68,36.13 124.99,34.82 124.99,33.2C124.99,31.58 123.68,30.27 122.07,30.27H94.73V2.93C94.73,1.31 93.42,0 91.8,0C90.18,0 88.87,1.31 88.87,2.93V21.21H83.01V2.93C83.01,1.31 81.7,0 80.08,0C78.46,0 77.15,1.31 77.15,2.93V21.21H71.29V2.93C71.29,1.32 69.98,0 68.36,0C66.74,0 65.43,1.32 65.43,2.93V21.21H59.57V2.93C59.57,1.32 58.26,0 56.64,0C55.02,0 53.71,1.32 53.71,2.93V21.21H47.85V2.93C47.85,1.31 46.54,0 44.92,0C43.3,0 41.99,1.31 41.99,2.93V21.21H36.13V2.93C36.13,1.31 34.82,0 33.2,0C31.58,0 30.27,1.31 30.27,2.93V30.27H2.93C1.31,30.27 0,31.58 0,33.2C0,34.82 1.31,36.13 2.93,36.13H21.21V41.99H2.93C1.31,41.99 0,43.3 0,44.92C0,46.54 1.31,47.85 2.93,47.85H21.21V53.71H2.93C1.32,53.71 0,55.02 0,56.64C0,58.26 1.32,59.57 2.93,59.57H21.21V65.43H2.93C1.32,65.43 0,66.74 0,68.36C0,69.98 1.32,71.29 2.93,71.29H21.21V77.15H2.93C1.31,77.15 0,78.46 0,80.08C0,81.7 1.31,83.01 2.93,83.01H21.21V88.87H2.93C1.31,88.87 0,90.18 0,91.8C0,93.42 1.31,94.73 2.93,94.73H30.27V122.07C30.27,123.68 31.58,124.99 33.2,124.99C34.82,124.99 36.13,123.68 36.13,122.07V103.79H41.99V122.07C41.99,123.68 43.3,125 44.92,125C46.54,125 47.85,123.68 47.85,122.07V103.79H53.71V122.07C53.71,123.69 55.02,125 56.64,125C58.26,125 59.57,123.69 59.57,122.07V103.79H65.43V122.07C65.43,123.69 66.74,125 68.36,125C69.98,125 71.29,123.69 71.29,122.07V103.79H77.15V122.07C77.15,123.68 78.46,125 80.08,125C81.7,125 83.01,123.68 83.01,122.07V103.79H88.87V122.07C88.87,123.68 90.18,124.99 91.8,124.99C93.42,124.99 94.73,123.68 94.73,122.07V94.73H122.07C123.68,94.73 124.99,93.42 124.99,91.8C124.99,90.18 123.68,88.87 122.07,88.87H103.79V83.01H122.07C123.68,83.01 125,81.7 125,80.08C125,78.46 123.68,77.15 122.07,77.15H103.79V71.29H122.07Z"
android:fillColor="#212121"/>
<path
android:pathData="M95.7,107.42H29.29C22.82,107.42 17.58,102.18 17.58,95.7V29.3C17.58,22.83 22.82,17.58 29.29,17.58H95.7C102.17,17.58 107.42,22.83 107.42,29.3V95.7C107.42,102.18 102.17,107.42 95.7,107.42Z"
android:fillColor="#3C3B3B"/>
<path
android:pathData="M91.79,29.3H25.39C23.23,29.3 21.48,31.05 21.48,33.2V95.7C21.48,100.02 24.98,103.52 29.29,103.52H91.79C93.95,103.52 95.7,101.77 95.7,99.61V33.2C95.7,31.05 93.95,29.3 91.79,29.3V29.3Z"
android:fillColor="#212121"/>
<path
android:pathData="M95.7,25.39H29.29C27.14,25.39 25.39,27.14 25.39,29.3V95.7C25.39,97.86 27.14,99.61 29.29,99.61H95.7C97.86,99.61 99.61,97.86 99.61,95.7V29.3C99.61,27.14 97.86,25.39 95.7,25.39V25.39Z"
android:fillColor="#FFE418"/>
<path
android:pathData="M29.29,25.39C27.14,25.39 25.39,27.14 25.39,29.3V95.7C25.39,97.86 27.14,99.61 29.29,99.61H87.89L37.11,25.39H29.29V25.39Z"
android:fillColor="#FFCB15"/>
<path
android:pathData="M39,53V71.53"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M39,53H46.25C48.87,53 51,55.13 51,57.75V57.75C51,60.37 48.87,62.5 46.25,62.5H39"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M39,62.5H47.25C49.87,62.5 52,64.63 52,67.25V67.25C52,69.87 49.87,72 47.25,72H39"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M58,53V71.53"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M58,72H70"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M75,53V71.53"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M75,72H87"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M75,62.5H87"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
<path
android:pathData="M75,53H87"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:strokeLineCap="round"/>
</group>
</group>
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="android:windowLightStatusBar">true</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="windowSplashScreenAnimationDuration">200</item>
<item name="postSplashScreenTheme">@style/Theme.Ble</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">#ffffff</item>
</style>
<style name="Theme.Ble" parent="android:Theme.Material.Light.NoActionBar" >
<item name="android:statusBarColor">#00000000</item>
<item name="android:navigationBarColor">#ffffffff</item>
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">Ble</string>
<string name="app_name">Arma BLE</string>
</resources>

View File

@ -1,9 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="android:windowLightStatusBar">true</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="windowSplashScreenAnimationDuration">200</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">#ffffff</item>
<item name="postSplashScreenTheme">@style/Theme.Ble</item>
</style>
<style name="Theme.Ble" parent="android:Theme.Material.Light.NoActionBar" >
<item name="android:statusBarColor">#00000000</item>
<item name="android:navigationBarColor">#00ffffff</item>
<item name="android:navigationBarColor">#ffffffff</item>
<item name="android:windowLightStatusBar" >true</item>
</style>
</resources>