Checkout
|
|
@ -50,20 +50,21 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
||||||
implementation 'androidx.activity:activity-compose:1.3.1'
|
implementation 'androidx.activity:activity-compose:1.7.0'
|
||||||
implementation "androidx.compose.ui:ui:$compose_version"
|
implementation "androidx.compose.ui:ui:1.5.0-alpha01"
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-alpha01"
|
||||||
implementation 'androidx.compose.material3:material3:1.1.0-beta01'
|
implementation 'androidx.compose.material3:material3:1.1.0-beta01'
|
||||||
|
implementation 'androidx.compose.material:material:1.5.0-alpha01'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0-alpha01"
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
debugImplementation "androidx.compose.ui:ui-tooling:1.5.0-alpha01"
|
||||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
|
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.core:core-splashscreen:1.0.0'
|
||||||
implementation 'androidx.navigation:navigation-compose:2.5.3'
|
implementation 'androidx.navigation:navigation-compose:2.5.3'
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
android:maxSdkVersion="30" />
|
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_CONNECT"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
android:usesPermissionFlags="neverForLocation"
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
|
@ -22,14 +25,14 @@
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Ble"
|
android:theme="@style/Theme.App.Starting"
|
||||||
tools:targetApi="31"
|
tools:targetApi="31"
|
||||||
android:name=".app.framework.App">
|
android:name=".app.framework.App">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".app.ui.MainActivity"
|
android:name=".app.ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.Ble">
|
android:theme="@style/Theme.App.Starting">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,162 @@
|
||||||
package llc.arma.ble.app.ui
|
package llc.arma.ble.app.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
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.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.screen.main.MainScreen
|
||||||
import llc.arma.ble.app.ui.theme.BleTheme
|
import llc.arma.ble.app.ui.theme.BleTheme
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
installSplashScreen()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
|
||||||
BleTheme {
|
BleTheme {
|
||||||
|
|
||||||
Surface(
|
val modalState =
|
||||||
modifier = Modifier
|
rememberModalBottomSheetState(
|
||||||
.fillMaxSize()
|
skipHalfExpanded = true,
|
||||||
.navigationBarsPadding(),
|
initialValue = ModalBottomSheetValue.Hidden
|
||||||
color = MaterialTheme.colorScheme.background
|
)
|
||||||
|
|
||||||
|
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) {
|
ModalBottomSheetLayout(
|
||||||
rememberMultiplePermissionsState(
|
sheetShape = RoundedCornerShape(
|
||||||
listOf(
|
topStart = 25.dp,
|
||||||
android.Manifest.permission.BLUETOOTH_SCAN,
|
topEnd = 25.dp
|
||||||
android.Manifest.permission.BLUETOOTH_CONNECT
|
),
|
||||||
)
|
sheetElevation = 0.dp,
|
||||||
)
|
sheetState = modalState,
|
||||||
} else {
|
sheetContent = {
|
||||||
rememberMultiplePermissionsState(
|
|
||||||
listOf()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.ViewEvent
|
||||||
import llc.arma.ble.app.ui.common.ViewSideEffect
|
import llc.arma.ble.app.ui.common.ViewSideEffect
|
||||||
import llc.arma.ble.app.ui.common.ViewState
|
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
|
import llc.arma.ble.domain.model.Ble
|
||||||
|
|
||||||
class BeaconContract {
|
class BeaconContract {
|
||||||
|
|
||||||
sealed class Event : ViewEvent {
|
sealed class Event : ViewEvent {
|
||||||
|
|
||||||
|
object OnWriteBle : Event()
|
||||||
|
|
||||||
|
object OnHideWriteBlePreview : Event()
|
||||||
|
|
||||||
|
object OnShowWriteBlePreview : Event()
|
||||||
|
|
||||||
|
object OnPowerEdit : Event()
|
||||||
|
|
||||||
data class OnBleChanged(
|
data class OnBleChanged(
|
||||||
val ble: Ble.Beacon
|
val ble: Ble.Beacon
|
||||||
) : Event()
|
) : Event()
|
||||||
|
|
||||||
|
data class OnPowerChanged(
|
||||||
|
val tx: BleView.BleState.TX
|
||||||
|
) : Event()
|
||||||
|
|
||||||
data class OnTxChanged(val tx: Int) : Event()
|
data class OnTxChanged(val tx: Int) : Event()
|
||||||
|
|
||||||
object OnNavigateUpClicked : Event()
|
object OnNavigateUpClicked : Event()
|
||||||
|
|
||||||
|
object OnChangePassword : Event()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class State : ViewState {
|
sealed class State : ViewState {
|
||||||
|
|
@ -24,15 +40,45 @@ class BeaconContract {
|
||||||
object Loading : State()
|
object Loading : State()
|
||||||
|
|
||||||
data class Display(
|
data class Display(
|
||||||
val beacon: Ble.Beacon
|
val origin: Ble.Beacon,
|
||||||
) : State()
|
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 {
|
sealed class Effect : ViewSideEffect {
|
||||||
|
|
||||||
|
object ShowPowerPicker : Effect()
|
||||||
|
|
||||||
|
object HidePowerPicker : Effect()
|
||||||
|
|
||||||
|
object HideWriteBlePreview : Effect()
|
||||||
|
|
||||||
|
object ShowWriteBlePreview : Effect()
|
||||||
|
|
||||||
sealed class Navigation : Effect() {
|
sealed class Navigation : Effect() {
|
||||||
|
|
||||||
|
object NavigateToChangePassword : Navigation()
|
||||||
|
|
||||||
object NavigateUp : Navigation()
|
object NavigateUp : Navigation()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,33 @@
|
||||||
package llc.arma.ble.app.ui.screen.beacon
|
package llc.arma.ble.app.ui.screen.beacon
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material.icons.rounded.KeyboardArrowRight
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
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.Ble
|
||||||
import llc.arma.ble.domain.model.BleInfo
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
enum class SheetPage {
|
||||||
|
WRITE, POWER_EDIT
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BeaconScreen(
|
fun BeaconScreen(
|
||||||
ble: Ble.Beacon,
|
ble: Ble.Beacon,
|
||||||
|
|
@ -34,10 +37,32 @@ fun BeaconScreen(
|
||||||
val viewModel = hiltViewModel<BeaconViewModel>()
|
val viewModel = hiltViewModel<BeaconViewModel>()
|
||||||
val state = viewModel.viewState.value
|
val state = viewModel.viewState.value
|
||||||
|
|
||||||
|
var sheetPage by rememberSaveable {
|
||||||
|
mutableStateOf<SheetPage?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bottomDialog = rememberBottomDialogState()
|
||||||
|
|
||||||
LaunchedEffect("effect"){
|
LaunchedEffect("effect"){
|
||||||
viewModel.effect.onEach {
|
viewModel.effect.onEach {
|
||||||
when(it){
|
when(it){
|
||||||
is BeaconContract.Effect.Navigation -> onNavigationEvent(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)
|
}.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
@ -46,29 +71,291 @@ fun BeaconScreen(
|
||||||
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
|
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
LaunchedEffect(sheetPage){
|
||||||
|
when(sheetPage){
|
||||||
CenterAlignedTopAppBar(
|
SheetPage.WRITE -> bottomDialog.show {
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
val scope = rememberCoroutineScope()
|
||||||
onClick = {
|
|
||||||
viewModel.setEvent(BeaconContract.Event.OnNavigateUpClicked)
|
val currentState = viewModel.viewState.value
|
||||||
},
|
|
||||||
content = {
|
if(currentState is BeaconContract.State.Display) {
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.ArrowBack,
|
Column() {
|
||||||
contentDescription = null
|
|
||||||
)
|
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){
|
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()
|
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
|
package llc.arma.ble.app.ui.screen.beacon
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import llc.arma.ble.app.ui.common.BaseViewModel
|
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.Ble
|
||||||
import llc.arma.ble.domain.model.BleInfo
|
import llc.arma.ble.domain.model.BleInfo
|
||||||
|
import llc.arma.ble.domain.usecase.WriteBle
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BeaconViewModel @Inject constructor(
|
class BeaconViewModel @Inject constructor(
|
||||||
|
private val bleMapper: BleMapper,
|
||||||
|
private val writeBle: WriteBle,
|
||||||
|
private val bleViewMapper: BleViewMapper
|
||||||
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
|
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
|
||||||
|
|
||||||
override fun setInitialState() = BeaconContract.State.Loading
|
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.OnNavigateUpClicked -> reduce(viewState.value, event)
|
||||||
is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event)
|
is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event)
|
||||||
is BeaconContract.Event.OnBleChanged -> 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(
|
private fun reduce(
|
||||||
state: BeaconContract.State,
|
state: BeaconContract.State,
|
||||||
event: BeaconContract.Event.OnNavigateUpClicked
|
event: BeaconContract.Event.OnNavigateUpClicked
|
||||||
|
|
@ -45,9 +86,104 @@ class BeaconViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
setState {
|
setState {
|
||||||
BeaconContract.State.Display(
|
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 {
|
sealed class Event : ViewEvent {
|
||||||
|
|
||||||
|
object RefreshBle : Event()
|
||||||
|
|
||||||
object OnNavigateUp : Event()
|
object OnNavigateUp : Event()
|
||||||
|
|
||||||
data class OnBeaconNavigationEvent(
|
data class OnBeaconNavigationEvent(
|
||||||
|
|
@ -44,6 +46,11 @@ class ConnectionContract {
|
||||||
sealed class Navigation : Effect() {
|
sealed class Navigation : Effect() {
|
||||||
|
|
||||||
object NavigateUp : Navigation()
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import kotlinx.coroutines.flow.launchIn
|
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.model.BleView
|
||||||
import llc.arma.ble.app.ui.screen.BleInfoView
|
import llc.arma.ble.app.ui.screen.BleInfoView
|
||||||
import llc.arma.ble.app.ui.screen.beacon.BeaconScreen
|
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.ThermometerContract
|
||||||
import llc.arma.ble.app.ui.screen.thermometer.ThermometerScreen
|
import llc.arma.ble.app.ui.screen.thermometer.ThermometerScreen
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
|
|
@ -82,16 +84,20 @@ fun ConnectionScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
when (state) {
|
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.Loading -> LoadingState()
|
||||||
is ConnectionContract.State.Display -> {
|
is ConnectionContract.State.Display -> {
|
||||||
when(state.ble){
|
when(state.ble){
|
||||||
is Ble.Beacon -> {}/*BeaconScreen(
|
is Ble.Beacon -> BeaconScreen(
|
||||||
ble = state.ble,
|
ble = state.ble,
|
||||||
onNavigationEvent = {
|
onNavigationEvent = {
|
||||||
viewModel.setEvent(ConnectionContract.Event.OnBeaconNavigationEvent(it))
|
viewModel.setEvent(ConnectionContract.Event.OnBeaconNavigationEvent(it))
|
||||||
}
|
}
|
||||||
)*/
|
)
|
||||||
is Ble.Thermometer -> {
|
is Ble.Thermometer -> {
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
|
@ -100,9 +106,7 @@ fun ConnectionScreen(
|
||||||
ble = state.ble,
|
ble = state.ble,
|
||||||
onNavigationEvent = {
|
onNavigationEvent = {
|
||||||
viewModel.setEvent(
|
viewModel.setEvent(
|
||||||
ConnectionContract.Event.OnThermometerNavigationEvent(
|
ConnectionContract.Event.OnThermometerNavigationEvent(it)
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -135,10 +139,50 @@ private fun LoadingState(){
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisplayException(
|
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
|
@HiltViewModel
|
||||||
class ConnectionViewModel @Inject constructor(
|
class ConnectionViewModel @Inject constructor(
|
||||||
savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
getBleBySerial: GetBleBySerial,
|
private val getBleBySerial: GetBleBySerial,
|
||||||
private val writeBle: WriteBle,
|
|
||||||
private val bleMapper: BleMapper,
|
|
||||||
private val bleViewMapper: BleViewMapper
|
|
||||||
) : BaseViewModel<ConnectionContract.State, ConnectionContract.Event, ConnectionContract.Effect>() {
|
) : BaseViewModel<ConnectionContract.State, ConnectionContract.Event, ConnectionContract.Effect>() {
|
||||||
|
|
||||||
init {
|
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")
|
val serial = savedStateHandle.get<String>("serial")
|
||||||
|
|
||||||
if(serial != null){
|
if(serial != null){
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
setState {
|
||||||
|
ConnectionContract.State.Loading
|
||||||
|
}
|
||||||
|
|
||||||
getBleBySerial(serial).fold(
|
getBleBySerial(serial).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
|
||||||
|
|
@ -50,54 +123,6 @@ class ConnectionViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
throw IllegalArgumentException("serial arg must not be null")
|
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
|
package llc.arma.ble.app.ui.screen.main
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.dialog
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import llc.arma.ble.app.ui.screen.beacon.BeaconContract
|
import llc.arma.ble.app.ui.screen.beacon.BeaconContract
|
||||||
import llc.arma.ble.app.ui.screen.thermometer.ThermometerScreen
|
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.ble.BleListScreen
|
||||||
import llc.arma.ble.app.ui.screen.connection.ConnectionContract
|
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.connection.ConnectionScreen
|
||||||
|
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
|
||||||
|
import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen() {
|
fun MainScreen() {
|
||||||
|
|
@ -45,6 +49,7 @@ fun MainScreen() {
|
||||||
onNavigationEvent = {
|
onNavigationEvent = {
|
||||||
when(it){
|
when(it){
|
||||||
ConnectionContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
|
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 OnHideTemperatureHistory : Event()
|
||||||
|
|
||||||
|
object OnChangePassword : Event()
|
||||||
|
|
||||||
data class OnSaveHistoryChanged(
|
data class OnSaveHistoryChanged(
|
||||||
val saveHistory: Boolean
|
val saveHistory: Boolean
|
||||||
) : Event()
|
) : Event()
|
||||||
|
|
@ -68,6 +70,8 @@ class ThermometerContract {
|
||||||
|
|
||||||
object Success : WriteState()
|
object Success : WriteState()
|
||||||
|
|
||||||
|
object Failure : WriteState()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -88,10 +92,16 @@ class ThermometerContract {
|
||||||
|
|
||||||
object HidePowerPicker : Effect()
|
object HidePowerPicker : Effect()
|
||||||
|
|
||||||
|
object ShowWriteBle : Effect()
|
||||||
|
|
||||||
|
object HideWriteBle : Effect()
|
||||||
|
|
||||||
sealed class Navigation : Effect() {
|
sealed class Navigation : Effect() {
|
||||||
|
|
||||||
object NavigateUp : Navigation()
|
object NavigateUp : Navigation()
|
||||||
|
|
||||||
|
object NavigateToChangePassword : Navigation()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,41 @@
|
||||||
package llc.arma.ble.app.ui.screen.thermometer
|
package llc.arma.ble.app.ui.screen.thermometer
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.Icons
|
||||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
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.app.ui.screen.thermometer.view.*
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
|
|
||||||
enum class SheetPage {
|
enum class SheetPage {
|
||||||
INTERVAL, POWER, TEMPERATURE_HISTORY
|
INTERVAL, POWER, TEMPERATURE_HISTORY, WRITE
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Boolean.localizedName: String
|
val Boolean.localizedName: String
|
||||||
get() {
|
get() {
|
||||||
return if(this){
|
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() {
|
get() {
|
||||||
return when(this){
|
return when(this){
|
||||||
Ble.BleState.TX.MINUS_40 -> -40
|
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
|
@Composable
|
||||||
fun ThermometerScreen(
|
fun ThermometerScreen(
|
||||||
ble: Ble.Thermometer,
|
ble: Ble.Thermometer,
|
||||||
onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
|
onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var sheetPage by remember {
|
var sheetPage by rememberSaveable {
|
||||||
mutableStateOf<SheetPage?>(null)
|
mutableStateOf<SheetPage?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewModel = hiltViewModel<ThermometerViewModel>()
|
val viewModel = hiltViewModel<ThermometerViewModel>()
|
||||||
val state = viewModel.viewState.value
|
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)
|
val writeSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
LaunchedEffect("effect"){
|
LaunchedEffect("effect"){
|
||||||
viewModel.effect.onEach {
|
viewModel.effect.onEach {
|
||||||
when(it){
|
when(it){
|
||||||
is ThermometerContract.Effect.Navigation -> onNavigationEvent(it)
|
is ThermometerContract.Effect.Navigation -> {
|
||||||
is ThermometerContract.Effect.HideIntervalPicker -> launch {
|
|
||||||
bottomSheetState.hide()
|
|
||||||
sheetPage = null
|
sheetPage = null
|
||||||
|
onNavigationEvent(it)
|
||||||
|
}
|
||||||
|
is ThermometerContract.Effect.HideIntervalPicker -> launch {
|
||||||
|
sheetPage = null
|
||||||
|
delay(100)
|
||||||
}
|
}
|
||||||
is ThermometerContract.Effect.ShowIntervalPicker -> launch {
|
is ThermometerContract.Effect.ShowIntervalPicker -> launch {
|
||||||
|
sheetPage = null
|
||||||
|
delay(100)
|
||||||
sheetPage = SheetPage.INTERVAL
|
sheetPage = SheetPage.INTERVAL
|
||||||
}
|
}
|
||||||
is ThermometerContract.Effect.HidePowerPicker -> launch {
|
is ThermometerContract.Effect.HidePowerPicker -> launch {
|
||||||
bottomSheetState.hide()
|
|
||||||
sheetPage = null
|
sheetPage = null
|
||||||
|
delay(100)
|
||||||
}
|
}
|
||||||
is ThermometerContract.Effect.ShowPowerPicker -> launch {
|
is ThermometerContract.Effect.ShowPowerPicker -> launch {
|
||||||
|
sheetPage = null
|
||||||
|
delay(100)
|
||||||
sheetPage = SheetPage.POWER
|
sheetPage = SheetPage.POWER
|
||||||
}
|
}
|
||||||
is ThermometerContract.Effect.HideTemperatureHistory -> launch {
|
is ThermometerContract.Effect.HideTemperatureHistory -> launch {
|
||||||
bottomSheetState.hide()
|
|
||||||
sheetPage = null
|
sheetPage = null
|
||||||
|
delay(100)
|
||||||
}
|
}
|
||||||
is ThermometerContract.Effect.ShowTemperatureHistory -> launch {
|
is ThermometerContract.Effect.ShowTemperatureHistory -> launch {
|
||||||
|
sheetPage = null
|
||||||
|
delay(100)
|
||||||
sheetPage = SheetPage.TEMPERATURE_HISTORY
|
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)
|
}.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.OnShowWriteBlePreview -> reduce(viewState.value, event)
|
||||||
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
|
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
|
||||||
is ThermometerContract.Event.OnWriteBle -> 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)
|
writeBle(state.thermometer.info.serial, it.writeRequest).fold(
|
||||||
|
onSuccess = {
|
||||||
setState {
|
setState {
|
||||||
state.copy(
|
state.copy(
|
||||||
writeState = ThermometerContract.State.Display.WriteState.Success
|
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.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
|
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
|
||||||
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
|
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
|
||||||
import com.patrykandpatrick.vico.compose.chart.Chart
|
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.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.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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import llc.arma.ble.app.ui.common.BaseViewModel
|
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.model.BleInfo
|
||||||
import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
|
import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.random.nextInt
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
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.compose.chart.scroll.rememberChartScrollState
|
||||||
import com.patrykandpatrick.vico.core.axis.AxisPosition
|
import com.patrykandpatrick.vico.core.axis.AxisPosition
|
||||||
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
|
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
|
||||||
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
|
|
||||||
import com.patrykandpatrick.vico.core.entry.ChartEntry
|
import com.patrykandpatrick.vico.core.entry.ChartEntry
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -70,26 +61,39 @@ fun TemperatureHistory(
|
||||||
val viewModel = hiltViewModel<TemperatureHistoryViewModel>()
|
val viewModel = hiltViewModel<TemperatureHistoryViewModel>()
|
||||||
val state = viewModel.viewState.value
|
val state = viewModel.viewState.value
|
||||||
|
|
||||||
LaunchedEffect(ble.serial) {
|
LaunchedEffect("ble.serial") {
|
||||||
viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial))
|
viewModel.setEvent(TemperatureHistoryContract.Event.OnStart(ble.serial))
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxHeight(0.9f)
|
||||||
|
) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
text = "История измерений",
|
text = title,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial))
|
viewModel.setEvent(TemperatureHistoryContract.Event.OnRefreshHistory(ble.serial))
|
||||||
},
|
},
|
||||||
enabled = when(state){
|
enabled = when(state){
|
||||||
is TemperatureHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
is TemperatureHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
||||||
|
|
@ -106,95 +110,90 @@ fun TemperatureHistory(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
when(state){
|
Box(modifier = Modifier) {
|
||||||
is TemperatureHistoryContract.State.Display -> Display(state = state)
|
|
||||||
TemperatureHistoryContract.State.Exception -> Exception()
|
when (state) {
|
||||||
|
is TemperatureHistoryContract.State.Display -> Display(state = state)
|
||||||
|
TemperatureHistoryContract.State.Exception -> Exception()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Display(
|
fun Display(
|
||||||
state: TemperatureHistoryContract.State.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 ->
|
when (state.loadingHistoryState) {
|
||||||
TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let {
|
|
||||||
ChartEntryModelProducer(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
|
is ProgressState.Finished -> {
|
||||||
(chartValues.chartEntryModel.entries.first().getOrNull(value.toInt()) as? TemperatureEntry)
|
|
||||||
?.localDate
|
|
||||||
?.let { formatter.format(Date(it)) }
|
|
||||||
.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
val lineChart = lineChart(
|
val producer = remember(state.loadingHistoryState.data) {
|
||||||
spacing = 110.dp
|
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()
|
val scrollState = rememberChartScrollState()
|
||||||
|
|
||||||
LaunchedEffect(scrollState.maxValue){
|
|
||||||
scrollState.scrollBy(scrollState.maxValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
Chart(
|
Chart(
|
||||||
chartScrollState = scrollState,
|
chartScrollState = scrollState,
|
||||||
chart = lineChart,
|
chart = lineChart,
|
||||||
chartModelProducer = producer,
|
chartModelProducer = producer,
|
||||||
startAxis = startAxis(),
|
startAxis = startAxis(),
|
||||||
bottomAxis = bottomAxis(
|
bottomAxis = bottomAxis(
|
||||||
|
tickLength = 0.dp,
|
||||||
valueFormatter = axisValueFormatter,
|
valueFormatter = axisValueFormatter,
|
||||||
labelRotationDegrees = 0f,
|
labelRotationDegrees = -90f,
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(1.5f),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
LaunchedEffect(scrollState.maxValue) {
|
||||||
}
|
scrollState.scrollBy(scrollState.maxValue)
|
||||||
is ProgressState.Indeterminate -> {
|
|
||||||
|
|
||||||
Box(modifier = Modifier.padding(8.dp)) {
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(2f),
|
|
||||||
){
|
|
||||||
|
|
||||||
CircularProgressIndicator(
|
|
||||||
strokeCap = StrokeCap.Round,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
is ProgressState.Indeterminate -> {
|
||||||
is ProgressState.Progress -> Box(modifier = Modifier.padding(8.dp)) {
|
|
||||||
|
|
||||||
Box(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier
|
strokeCap = StrokeCap.Round,
|
||||||
.fillMaxWidth()
|
modifier = Modifier.align(Alignment.Center)
|
||||||
.aspectRatio(2f),
|
)
|
||||||
){
|
|
||||||
|
}
|
||||||
|
is ProgressState.Progress -> {
|
||||||
|
|
||||||
val progressAnimDuration = 1500
|
val progressAnimDuration = 1500
|
||||||
val progressAnimation by animateFloatAsState(
|
val progressAnimation by animateFloatAsState(
|
||||||
targetValue = state.loadingHistoryState.value,
|
targetValue = state.loadingHistoryState.value,
|
||||||
animationSpec = tween(durationMillis = progressAnimDuration, easing = FastOutSlowInEasing)
|
animationSpec = tween(
|
||||||
|
durationMillis = progressAnimDuration,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
|
@ -206,6 +205,7 @@ fun Display(
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,7 +234,11 @@ class TemperatureHistoryContract {
|
||||||
|
|
||||||
sealed class Event : ViewEvent {
|
sealed class Event : ViewEvent {
|
||||||
|
|
||||||
data class LoadHistory(
|
data class OnStart(
|
||||||
|
val serial: String
|
||||||
|
) : Event()
|
||||||
|
|
||||||
|
data class OnRefreshHistory(
|
||||||
val serial: String
|
val serial: String
|
||||||
) : Event()
|
) : Event()
|
||||||
|
|
||||||
|
|
@ -269,13 +273,50 @@ class TemperatureHistoryViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun handleEvents(event: TemperatureHistoryContract.Event) {
|
override fun handleEvents(event: TemperatureHistoryContract.Event) {
|
||||||
when(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(
|
private fun reduce(
|
||||||
state: TemperatureHistoryContract.State,
|
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 {
|
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.model.BleInfo
|
||||||
import llc.arma.ble.domain.repository.BleRepository
|
import llc.arma.ble.domain.repository.BleRepository
|
||||||
import llc.arma.ble.domain.usecase.GetBleBySerial
|
import llc.arma.ble.domain.usecase.GetBleBySerial
|
||||||
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
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
|
@Singleton
|
||||||
class BleRepositoryImpl @Inject constructor(
|
class BleRepositoryImpl @Inject constructor(
|
||||||
private val app: Application
|
private val app: Application
|
||||||
|
|
@ -75,15 +87,12 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override fun getBleAroundFlow(): Flow<Result<List<BleInfo>, BleException>> {
|
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,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_SCAN
|
Manifest.permission.BLUETOOTH_SCAN
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
){
|
||||||
|
|
||||||
flow { emit(Result.failure(BleException.PermissionDenied)) }
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
|
|
||||||
|
|
@ -96,7 +105,7 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
super.onScanResult(callbackType, result)
|
super.onScanResult(callbackType, result)
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
|
||||||
app,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
@ -143,7 +152,7 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
send(Result.success(resultList.values.toList()))
|
send(Result.success(resultList.values.toList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 100, 500)
|
}, 500, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitClose {
|
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 ->
|
deviceCache[serial]?.let { result ->
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
|
||||||
app,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
@ -264,8 +277,8 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
val dataResult = readCharacteristic(
|
val dataResult = readCharacteristic(
|
||||||
device = record.device,
|
device = record.device,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
serviceId = serviceUUID,
|
||||||
characteristicId = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
|
characteristicId = temperatureReadUUID
|
||||||
).fold(
|
).fold(
|
||||||
onFailure = {
|
onFailure = {
|
||||||
return Result.failure(it)
|
return Result.failure(it)
|
||||||
|
|
@ -283,15 +296,17 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
device = record.device,
|
device = record.device,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
serviceId = serviceUUID,
|
||||||
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
|
characteristicId = intervalReadUUID,
|
||||||
writeData = byteArrayOf(3, 0, 0, 0, 0)
|
writeData = byteArrayOf(3, 0, 0, 0, 0)
|
||||||
)
|
).onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
val dataResult = readCharacteristic(
|
val dataResult = readCharacteristic(
|
||||||
device = record.device,
|
device = record.device,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
serviceId = serviceUUID,
|
||||||
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
|
characteristicId = intervalReadUUID
|
||||||
).fold(
|
).fold(
|
||||||
onFailure = {
|
onFailure = {
|
||||||
return Result.failure(it)
|
return Result.failure(it)
|
||||||
|
|
@ -311,221 +326,153 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override suspend fun getTemperatureHistoryBySerial(
|
override suspend fun getTemperatureHistoryBySerial(
|
||||||
serial: String
|
serial: String
|
||||||
): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> = flow {
|
): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> {
|
||||||
|
|
||||||
fun ByteArray.getUIntAt(idx: Int) =
|
var gatt: BluetoothGatt? = null
|
||||||
((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)
|
|
||||||
|
|
||||||
findDeviceBySerial(serial).fold(
|
return callbackFlow {
|
||||||
onSuccess = {
|
|
||||||
return@fold it
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
emit(Result.failure(it))
|
|
||||||
return@flow
|
|
||||||
}
|
|
||||||
).let { device ->
|
|
||||||
|
|
||||||
emit(Result.success(ProgressState.Indeterminate))
|
deviceCache[serial]?.device?.let {
|
||||||
|
|
||||||
writeCharacteristic(
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
|
||||||
device = device,
|
app,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
writeData = byteArrayOf(2)
|
) {
|
||||||
)
|
|
||||||
|
|
||||||
val countDataArray = readCharacteristic(
|
gatt = it.connectGatt(app, false, ReadHistoryCallback(app) {
|
||||||
device = device,
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
send(it)
|
||||||
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
|
}
|
||||||
).fold(
|
})
|
||||||
onFailure = {
|
|
||||||
emit(Result.failure(it))
|
|
||||||
return@flow
|
|
||||||
},
|
|
||||||
onSuccess = { return@fold it }
|
|
||||||
)
|
|
||||||
|
|
||||||
writeCharacteristic(
|
} else {
|
||||||
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))
|
|
||||||
|
|
||||||
|
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(
|
override suspend fun writeBle(
|
||||||
serial: String,
|
serial: String,
|
||||||
request: Ble.Thermometer.WriteRequest
|
request: Ble.Thermometer.WriteRequest
|
||||||
) {
|
): Result<Unit, BleException> {
|
||||||
|
|
||||||
deviceCache[serial]?.let { result ->
|
deviceCache[serial]?.let { result ->
|
||||||
|
|
||||||
request.tx?.let { writeTx(result.device, it) }
|
request.tx?.let { writeTx(result.device, it) }?.onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
request.historyInterval?.let { writeSaveInterval(result.device, it) }
|
request.historyInterval?.let { writeSaveInterval(result.device, it) }?.onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
request.saveHistory?.let { writeSaveEnabled(result.device, it) }
|
request.saveHistory?.let { writeSaveEnabled(result.device, it) }?.onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeToFlash(serial).onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
deviceCache.remove(serial)
|
deviceCache.remove(serial)
|
||||||
resultList.remove(serial)
|
resultList.remove(serial)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
return Result.success(Unit)
|
||||||
|
|
||||||
private suspend fun writeBeacon(ble: Ble.Beacon){
|
|
||||||
|
|
||||||
deviceCache[ble.info.serial]?.device?.let {
|
|
||||||
|
|
||||||
writeTx(it, ble.state.tx)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
private suspend fun writeTx(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
tx: Ble.BleState.TX
|
tx: Ble.BleState.TX
|
||||||
) {
|
): Result<Unit, BleException> {
|
||||||
|
|
||||||
writeCharacteristic(
|
return writeCharacteristic(
|
||||||
device = device,
|
device = device,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
serviceId = serviceUUID,
|
||||||
characteristicId = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb"),
|
characteristicId = txWriteUUID,
|
||||||
writeData = byteArrayOf(
|
writeData = byteArrayOf(
|
||||||
when(tx) {
|
when(tx) {
|
||||||
Ble.BleState.TX.MINUS_40 -> -40
|
Ble.BleState.TX.MINUS_40 -> -40
|
||||||
|
|
@ -546,17 +493,17 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
private suspend fun writeSaveInterval(
|
private suspend fun writeSaveInterval(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
interval: Long
|
interval: Long
|
||||||
) {
|
): Result<Unit, BleException> {
|
||||||
|
|
||||||
fun UInt.to4ByteArrayInBigEndian(): ByteArray =
|
fun UInt.to4ByteArrayInBigEndian(): ByteArray =
|
||||||
(3 downTo 0).map {
|
(3 downTo 0).map {
|
||||||
(this shr (it * Byte.SIZE_BITS)).toByte()
|
(this shr (it * Byte.SIZE_BITS)).toByte()
|
||||||
}.reversed().toByteArray()
|
}.reversed().toByteArray()
|
||||||
|
|
||||||
writeCharacteristic(
|
return writeCharacteristic(
|
||||||
device = device,
|
device = device,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
serviceId = serviceUUID,
|
||||||
characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"),
|
characteristicId = intervalWriteUUID,
|
||||||
writeData = mutableListOf<Byte>(3).apply {
|
writeData = mutableListOf<Byte>(3).apply {
|
||||||
addAll(interval.toUInt().to4ByteArrayInBigEndian().toList())
|
addAll(interval.toUInt().to4ByteArrayInBigEndian().toList())
|
||||||
}.toByteArray()
|
}.toByteArray()
|
||||||
|
|
@ -569,17 +516,15 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
enabled: Boolean
|
enabled: Boolean
|
||||||
): Result<Unit, BleException> {
|
): Result<Unit, BleException> {
|
||||||
|
|
||||||
writeCharacteristic(
|
return writeCharacteristic(
|
||||||
device = device,
|
device = device,
|
||||||
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
|
serviceId = serviceUUID,
|
||||||
characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"),
|
characteristicId = saveEnabledWriteUUID,
|
||||||
writeData = mutableListOf<Byte>(4).apply {
|
writeData = mutableListOf<Byte>(4).apply {
|
||||||
add(if(enabled) 1 else 0)
|
add(if(enabled) 1 else 0)
|
||||||
}.toByteArray()
|
}.toByteArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
return Result.success(Unit)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun readCharacteristic(
|
private suspend fun readCharacteristic(
|
||||||
|
|
@ -601,18 +546,30 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
Log.d("read", "onConnectionStateChange $newState $status")
|
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
|
characteristic.uuid == characteristicId
|
||||||
}?.let { char ->
|
}?.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,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
|
|
||||||
gatt.readCharacteristic(char)
|
gatt.readCharacteristic(char)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
it.resume(Result.failure(BleException.PermissionDenied))
|
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(
|
override fun onCharacteristicRead(
|
||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
characteristic: BluetoothGattCharacteristic,
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
|
@ -659,36 +646,40 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
Log.d("read", "onCharacteristicRead $status")
|
Log.d("read", "onCharacteristicRead $status")
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if(status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
app,
|
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
app,
|
||||||
) {
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
it.resume(Result.failure(BleException.PermissionDenied))
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
}else {
|
) {
|
||||||
gatt.close()
|
|
||||||
result = value
|
gatt.close()
|
||||||
if(result != null){
|
result = value
|
||||||
it.resume(Result.success(result!!))
|
it.resume(Result.success(result!!))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bleGatt?.close()
|
it.resume(Result.failure(BleException.PermissionDenied))
|
||||||
it.resume(Result.failure(BleException.UnexpectedResponse))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
it.resume(Result.failure(BleException.UnexpectedResponse))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
|
||||||
app,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED) {
|
||||||
) {
|
bleGatt = device.connectGatt(app, false, callback)
|
||||||
it.resume(Result.failure(BleException.PermissionDenied))
|
|
||||||
} else {
|
} else {
|
||||||
bleGatt = device.connectGatt(app, true, callback)
|
it.resume(Result.failure(BleException.PermissionDenied))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -698,7 +689,7 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
serviceId: UUID,
|
serviceId: UUID,
|
||||||
characteristicId: UUID,
|
characteristicId: UUID,
|
||||||
writeData: ByteArray
|
writeData: ByteArray
|
||||||
) = suspendCancellableCoroutine {
|
): Result<Unit, BleException> = suspendCancellableCoroutine {
|
||||||
|
|
||||||
var bleGatt: BluetoothGatt? = null
|
var bleGatt: BluetoothGatt? = null
|
||||||
|
|
||||||
|
|
@ -710,23 +701,35 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
newState: Int
|
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 {
|
} else {
|
||||||
|
|
||||||
if(newState == BluetoothProfile.STATE_DISCONNECTED && status == BluetoothGatt.GATT_FAILURE){
|
it.resume(Result.failure(BleException.UnexpectedResponse))
|
||||||
bleGatt?.close()
|
bleGatt?.close()
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -737,7 +740,9 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
status: Int
|
status: Int
|
||||||
) {
|
) {
|
||||||
super.onServicesDiscovered(gatt, status)
|
super.onServicesDiscovered(gatt, status)
|
||||||
|
|
||||||
Log.d("write", "onServicesDiscovered $status")
|
Log.d("write", "onServicesDiscovered $status")
|
||||||
|
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
|
||||||
gatt.services?.firstOrNull { service ->
|
gatt.services?.firstOrNull { service ->
|
||||||
|
|
@ -746,23 +751,30 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
characteristic.uuid == characteristicId
|
characteristic.uuid == characteristicId
|
||||||
}?.let { char ->
|
}?.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,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
gatt.writeCharacteristic(char, writeData)
|
||||||
gatt.writeCharacteristic(char, writeData, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
|
|
||||||
}else{
|
} else {
|
||||||
char.value = writeData
|
|
||||||
gatt.writeCharacteristic(char)
|
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")
|
Log.d("write", "onCharacteristicWrite $status")
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
|
||||||
app,
|
app,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
return
|
|
||||||
} else {
|
|
||||||
gatt.close()
|
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() {
|
fun BluetoothGatt.writeCharacteristic(
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
override fun onScanResult(
|
data: ByteArray
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 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
|
package llc.arma.ble.domain.usecase
|
||||||
|
|
||||||
import android.app.appsearch.SetSchemaRequest
|
import android.app.appsearch.SetSchemaRequest
|
||||||
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
import llc.arma.ble.domain.repository.BleRepository
|
import llc.arma.ble.domain.repository.BleRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -9,15 +10,18 @@ class WriteBle @Inject constructor(
|
||||||
private val bleRepository: BleRepository
|
private val bleRepository: BleRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(ble: Ble){
|
suspend operator fun invoke(
|
||||||
bleRepository.writeBle(ble)
|
serial: String,
|
||||||
|
request: Ble.Thermometer.WriteRequest
|
||||||
|
): llc.arma.ble.domain.Result<Unit, BleException>{
|
||||||
|
return bleRepository.writeBle(serial, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
serial: String,
|
serial: String,
|
||||||
request: Ble.Thermometer.WriteRequest
|
request: Ble.Beacon.WriteRequest
|
||||||
){
|
): llc.arma.ble.domain.Result<Unit, BleException>{
|
||||||
bleRepository.writeBle(serial, request)
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</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>
|
<resources>
|
||||||
<string name="app_name">Ble</string>
|
<string name="app_name">Arma BLE</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<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" >
|
<style name="Theme.Ble" parent="android:Theme.Material.Light.NoActionBar" >
|
||||||
<item name="android:statusBarColor">#00000000</item>
|
<item name="android:statusBarColor">#00000000</item>
|
||||||
<item name="android:navigationBarColor">#00ffffff</item>
|
<item name="android:navigationBarColor">#ffffffff</item>
|
||||||
<item name="android:windowLightStatusBar" >true</item>
|
<item name="android:windowLightStatusBar" >true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||