Checkout
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "Сохранить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Сохранить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Применить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "Повторить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Отменить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Отменить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Отменить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Ок"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = "Ок"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Ble</string>
|
||||
<string name="app_name">Arma BLE</string>
|
||||
</resources>
|
||||
|
|
@ -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>
|
||||