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

View File

@ -50,20 +50,21 @@ android {
dependencies { 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'

View File

@ -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" />

View File

@ -1,34 +1,122 @@
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 {
val modalState =
rememberModalBottomSheetState(
skipHalfExpanded = true,
initialValue = ModalBottomSheetValue.Hidden
)
var sheetContent by remember() {
mutableStateOf<@Composable () -> Unit>({})
}
CompositionLocalProvider(
LocalBottomDialogState provides BottomState(
sheetState = modalState,
setContent = {
sheetContent = it
}
)
) {
ModalBottomSheetLayout(
sheetShape = RoundedCornerShape(
topStart = 25.dp,
topEnd = 25.dp
),
sheetElevation = 0.dp,
sheetState = modalState,
sheetContent = {
val scope = rememberCoroutineScope()
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
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( Surface(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -36,16 +124,20 @@ class MainActivity : ComponentActivity() {
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
val multiplePermissionsState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val multiplePermissionsState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
rememberMultiplePermissionsState( rememberMultiplePermissionsState(
listOf( listOf(
android.Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT Manifest.permission.BLUETOOTH_CONNECT
) )
) )
} else { } else {
rememberMultiplePermissionsState( rememberMultiplePermissionsState(
listOf() listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
) )
} }
@ -64,6 +156,10 @@ class MainActivity : ComponentActivity() {
} }
} }
)
}
}
} }

View File

@ -0,0 +1,51 @@
package llc.arma.ble.app.ui.common
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
val LocalBottomDialogState = compositionLocalOf<BottomState?> { null }
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun rememberBottomDialogState(): BottomDialogState {
val state = LocalBottomDialogState.current
return remember {
BottomDialogState(
sheetState = state?.sheetState,
setContent = state?.setContent ?: { }
)
}
}
class BottomState @OptIn(ExperimentalMaterialApi::class) constructor(
val sheetState: ModalBottomSheetState?,
val setContent: (@Composable () -> Unit) -> Unit,
)
class BottomDialogState @OptIn(ExperimentalMaterialApi::class) constructor(
private val sheetState: ModalBottomSheetState?,
val setContent: (@Composable () -> Unit) -> Unit,
) {
@OptIn(ExperimentalMaterialApi::class)
suspend fun show(
content: @Composable () -> Unit
){
setContent(content)
if(sheetState?.currentValue != ModalBottomSheetValue.Expanded)
sheetState?.show()
}
@OptIn(ExperimentalMaterialApi::class)
suspend fun hide(){
sheetState?.hide()
setContent { }
}
}

View File

@ -3,20 +3,36 @@ package llc.arma.ble.app.ui.screen.beacon
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.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()
} }

View File

@ -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))
} }
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val scope = rememberCoroutineScope()
val currentState = viewModel.viewState.value
if(currentState is BeaconContract.State.Display) {
Column() {
when (currentState.writeState) {
is BeaconContract.State.Display.WriteState.DisplayPreview -> {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Записать изменения?",
style = MaterialTheme.typography.titleLarge
)
currentState.writeState.writeRequest.tx?.let {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${currentState.origin.state.tx.localizedName} db -> ${it.localizedName} db"
)
}
}
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
viewModel.setEvent(BeaconContract.Event.OnWriteBle)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Записать"
)
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
scope.launch {
viewModel.setEvent(BeaconContract.Event.OnHideWriteBlePreview)
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
is BeaconContract.State.Display.WriteState.Writing -> {
Box {
Column() {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Запись",
style = MaterialTheme.typography.titleLarge
)
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 48.dp)
)
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
scope.launch {
viewModel.setEvent(BeaconContract.Event.OnHideWriteBlePreview)
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}
BeaconContract.State.Display.WriteState.Success -> {
Box {
Column { Column {
CenterAlignedTopAppBar( Text(
navigationIcon = { modifier = Modifier.padding(horizontal = 12.dp),
IconButton( text = "Запись завершена",
onClick = { style = MaterialTheme.typography.titleLarge
viewModel.setEvent(BeaconContract.Event.OnNavigateUpClicked)
},
content = {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = null
)
}
)
},
title = {
if (state is BeaconContract.State.Display) Text(text = state.beacon.info.name)
}
) )
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))
}
}
}
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()
} }
@ -86,102 +373,3 @@ private fun LoadingState(){
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DisplayState(ble: Ble.Beacon){
Column {
LazyColumn(
modifier = Modifier.weight(1f),
content = {
item {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(bleInfo = ble.info)
}
}
item {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "-40 db"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
}
)
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
}
}
}
}

View File

@ -1,18 +1,27 @@
package llc.arma.ble.app.ui.screen.beacon 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
)
}
}
)
}
}
}
}
}
} }

View File

@ -0,0 +1,158 @@
package llc.arma.ble.app.ui.screen.beacon.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.beacon.BeaconContract
@Composable
fun DisplayState(
onEvent: (BeaconContract.Event) -> Unit,
ble: BleView.Beacon
) {
Column() {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(bleInfo = ble.info)
}
Column(
modifier = Modifier,
content = {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(BeaconContract.Event.OnPowerEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${ble.state.tx.value} db"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(BeaconContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
}
)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(BeaconContract.Event.OnShowWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
}
}
}
}

View File

@ -0,0 +1,97 @@
package llc.arma.ble.app.ui.screen.beacon.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
@Composable
fun PowerEdit(
state: BleView.Beacon,
onEvent: (BeaconContract.Event) -> Unit,
){
var value by remember(state.state.tx) {
mutableStateOf(state.state.tx)
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Мощность",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
BleView.BleState.TX.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { value = it }
.padding(4.dp)
) {
RadioButton(
selected = it == value,
onClick = { value = it }
)
Text(text = it.value.toString() + " db")
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(
BeaconContract.Event.OnPowerChanged(
value
)
)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
}

View File

@ -13,6 +13,8 @@ class ConnectionContract {
sealed class Event : ViewEvent { 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()
} }
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,62 @@
package llc.arma.ble.app.ui.screen.password
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
class ChangePasswordContract {
sealed class Event : ViewEvent {
data class OnPasswordChanged(
val password: String
) : Event()
data class OnRePasswordChanged(
val password: String
) : Event()
object OnChange : Event()
object OnNavigateUp : Event()
}
data class State(
val password: String,
val rePassword: String,
val exception: ValidationException?,
val loading: LoadingState?
) : ViewState {
sealed class LoadingState {
object Loading : LoadingState()
object Success : LoadingState()
object Failure : LoadingState()
}
sealed class ValidationException {
object PasswordsNotMatch : ValidationException()
object WrongLength : ValidationException()
}
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
object NavigateUp : Navigation()
}
}
}

View File

@ -0,0 +1,270 @@
package llc.arma.ble.app.ui.screen.password
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.screen.password.view.Loading
import llc.arma.ble.app.ui.screen.password.view.Result
import llc.arma.ble.app.ui.screen.thermometer.view.LoadingState
@Composable
fun ChangePasswordScreen(
onNavigationEvent: (ChangePasswordContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<ChangePasswordViewModel>()
val state = viewModel.viewState.value
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is ChangePasswordContract.Effect.Navigation -> onNavigationEvent(it)
}
}.launchIn(this)
}
Surface(
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
) {
Column {
Spacer(modifier = Modifier.height(20.dp))
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.titleLarge,
text = "Изменение пароля",
textAlign = TextAlign.Center
)
if(state.loading != null){
when(state.loading){
ChangePasswordContract.State.LoadingState.Loading -> {
Loading {
viewModel.setEvent(it)
}
}
ChangePasswordContract.State.LoadingState.Failure,
ChangePasswordContract.State.LoadingState.Success -> {
Result(
onEvent = {
viewModel.setEvent(it)
},
state = state.loading
)
}
}
} else {
Column(
modifier = Modifier.padding(20.dp)
) {
var passwordVisibility by remember {
mutableStateOf(false)
}
var rePasswordVisibility by remember {
mutableStateOf(false)
}
@Composable
fun TrailingPasswordIcon(
visible: Boolean,
onClick: () -> Unit
) {
IconButton(onClick = onClick) {
Icon(
contentDescription = null,
imageVector = if (visible) {
Icons.Rounded.Visibility
} else {
Icons.Rounded.VisibilityOff
}
)
}
}
val isError = state.exception != null
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (passwordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
value = state.password,
onValueChange = {
viewModel.setEvent(ChangePasswordContract.Event.OnPasswordChanged(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.NumberPassword,
imeAction = ImeAction.Next),
trailingIcon = {
TrailingPasswordIcon(visible = passwordVisibility) {
passwordVisibility = passwordVisibility.not()
}
},
isError = isError,
label = {
Text(text = "Пароль")
},
supportingText = {
Row() {
Spacer(modifier = Modifier.weight(1f))
Text(
style = MaterialTheme.typography.bodyMedium,
text = "${state.password.length}/6"
)
}
}
)
Spacer(modifier = Modifier.height(12.dp))
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (rePasswordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
value = state.rePassword,
onValueChange = {
viewModel.setEvent(ChangePasswordContract.Event.OnRePasswordChanged(it))
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
trailingIcon = {
TrailingPasswordIcon(visible = rePasswordVisibility) {
rePasswordVisibility = rePasswordVisibility.not()
}
},
label = {
Text(text = "Повторите пароль")
},
isError = isError,
supportingText = {
Row() {
if (isError) {
val text = when (state.exception) {
is ChangePasswordContract.State.ValidationException.WrongLength -> "Неверная длинна"
is ChangePasswordContract.State.ValidationException.PasswordsNotMatch -> "Пароли не совпадают"
null -> ""
}
Text(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
text = text
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
style = MaterialTheme.typography.bodyMedium,
text = "${state.rePassword.length}/6"
)
}
}
)
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
viewModel.setEvent(ChangePasswordContract.Event.OnChange)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
viewModel.setEvent(ChangePasswordContract.Event.OnNavigateUp)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,125 @@
package llc.arma.ble.app.ui.screen.password
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.usecase.ChangeBlePassword
import javax.inject.Inject
@HiltViewModel
class ChangePasswordViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val changeBlePassword: ChangeBlePassword
) : BaseViewModel<ChangePasswordContract.State, ChangePasswordContract.Event, ChangePasswordContract.Effect>() {
override fun setInitialState() = ChangePasswordContract.State("", "", null, null)
override fun handleEvents(event: ChangePasswordContract.Event) {
when(event){
is ChangePasswordContract.Event.OnPasswordChanged -> reduce(viewState.value, event)
is ChangePasswordContract.Event.OnRePasswordChanged -> reduce(viewState.value, event)
is ChangePasswordContract.Event.OnChange -> reduce(viewState.value, event)
is ChangePasswordContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: ChangePasswordContract.State,
event: ChangePasswordContract.Event.OnPasswordChanged
) {
setState {
copy(
password = event.password
)
}
}
private fun reduce(
state: ChangePasswordContract.State,
event: ChangePasswordContract.Event.OnRePasswordChanged
) {
setState {
copy(
rePassword = event.password
)
}
}
private fun reduce(
state: ChangePasswordContract.State,
event: ChangePasswordContract.Event.OnChange
) {
if(state.password.length != 6 || state.rePassword.length != 6){
setState {
state.copy(
exception = ChangePasswordContract.State.ValidationException.WrongLength
)
}
} else {
if(state.password != state.rePassword){
setState {
state.copy(
exception = ChangePasswordContract.State.ValidationException.PasswordsNotMatch
)
}
} else {
viewModelScope.launch {
setState {
state.copy(
loading = ChangePasswordContract.State.LoadingState.Loading,
exception = null
)
}
changeBlePassword.invoke(
state.password,
savedStateHandle.get<String>("serial")!!
).fold(
onSuccess = {
setState {
state.copy(
loading = ChangePasswordContract.State.LoadingState.Success,
exception = null
)
}
},
onFailure = {
setState {
state.copy(
loading = ChangePasswordContract.State.LoadingState.Failure,
exception = null
)
}
}
)
}
}
}
}
private fun reduce(
state: ChangePasswordContract.State,
event: ChangePasswordContract.Event.OnNavigateUp
) {
setEffect {
ChangePasswordContract.Effect.Navigation.NavigateUp
}
}
}

View File

@ -0,0 +1,205 @@
package llc.arma.ble.app.ui.screen.password.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
@Composable
fun Display(
onEvent: (ChangePasswordContract.Event) -> Unit,
state: ChangePasswordContract.State
){
Column(
modifier = Modifier.padding(20.dp)
) {
var passwordVisibility by remember {
mutableStateOf(false)
}
var rePasswordVisibility by remember {
mutableStateOf(false)
}
@Composable
fun TrailingPasswordIcon(
visible: Boolean,
onClick: () -> Unit
) {
IconButton(onClick = onClick) {
Icon(
contentDescription = null,
imageVector = if (visible) {
Icons.Rounded.Visibility
} else {
Icons.Rounded.VisibilityOff
}
)
}
}
val isError = state.exception != null
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (passwordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
value = state.password,
onValueChange = {
onEvent(ChangePasswordContract.Event.OnPasswordChanged(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.NumberPassword,
imeAction = ImeAction.Next),
trailingIcon = {
TrailingPasswordIcon(visible = passwordVisibility) {
passwordVisibility = passwordVisibility.not()
}
},
isError = isError,
label = {
Text(text = "Пароль")
},
supportingText = {
Row() {
Spacer(modifier = Modifier.weight(1f))
Text(
style = MaterialTheme.typography.bodyMedium,
text = "${state.password.length}/6"
)
}
}
)
Spacer(modifier = Modifier.height(12.dp))
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (rePasswordVisibility.not()) PasswordVisualTransformation() else VisualTransformation.None,
value = state.rePassword,
onValueChange = {
onEvent(ChangePasswordContract.Event.OnRePasswordChanged(it))
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
trailingIcon = {
TrailingPasswordIcon(visible = rePasswordVisibility) {
rePasswordVisibility = rePasswordVisibility.not()
}
},
label = {
Text(text = "Повторите пароль")
},
isError = isError,
supportingText = {
Row() {
if (isError) {
val text = when (state.exception) {
is ChangePasswordContract.State.ValidationException.WrongLength -> "Неверная длинна"
is ChangePasswordContract.State.ValidationException.PasswordsNotMatch -> "Пароли не совпадают"
null -> ""
}
Text(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
text = text
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
style = MaterialTheme.typography.bodyMedium,
text = "${state.rePassword.length}/6"
)
}
}
)
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(ChangePasswordContract.Event.OnChange)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(ChangePasswordContract.Event.OnNavigateUp)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}

View File

@ -0,0 +1,67 @@
package llc.arma.ble.app.ui.screen.password.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
@Composable
fun Loading(
onEvent: (ChangePasswordContract.Event) -> Unit
) {
Column(
modifier = Modifier.padding(20.dp)
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(ChangePasswordContract.Event.OnNavigateUp)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}

View File

@ -0,0 +1,115 @@
package llc.arma.ble.app.ui.screen.password.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
@Composable
fun Result(
onEvent: (ChangePasswordContract.Event) -> Unit,
state: ChangePasswordContract.State.LoadingState
) {
Column(
modifier = Modifier.padding(20.dp)
) {
when(state){
ChangePasswordContract.State.LoadingState.Failure -> {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_error),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
}
ChangePasswordContract.State.LoadingState.Success -> {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_done),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
}
else -> {}
}
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(ChangePasswordContract.Event.OnNavigateUp)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}

View File

@ -20,6 +20,8 @@ class ThermometerContract {
object OnHideTemperatureHistory : Event() object 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()
} }
} }

View File

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

View File

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

View File

@ -236,6 +236,42 @@ fun DisplayState(
} }
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(ThermometerContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
} }
) )

View File

@ -11,18 +11,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.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,6 +110,8 @@ fun TemperatureHistory(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) { when (state) {
is TemperatureHistoryContract.State.Display -> Display(state = state) is TemperatureHistoryContract.State.Display -> Display(state = state)
TemperatureHistoryContract.State.Exception -> Exception() TemperatureHistoryContract.State.Exception -> Exception()
@ -115,86 +121,79 @@ fun TemperatureHistory(
} }
}
@Composable @Composable
fun Display( fun Display(
state: TemperatureHistoryContract.State.Display state: TemperatureHistoryContract.State.Display
) { ) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) { when (state.loadingHistoryState) {
is ProgressState.Finished -> { is ProgressState.Finished -> {
Text(text = "${state.loadingHistoryState.data.size}") val producer = remember(state.loadingHistoryState.data) {
state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
val producer = state.loadingHistoryState.data.mapIndexed { index, measurePoint -> TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let { }.let {
ChartEntryModelProducer(it) ChartEntryModelProducer(it)
} }
}
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues -> val axisValueFormatter =
(chartValues.chartEntryModel.entries.first().getOrNull(value.toInt()) as? TemperatureEntry) AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.first()
.getOrNull(value.toInt()) as? TemperatureEntry)
?.localDate ?.localDate
?.let { formatter.format(Date(it)) } ?.let { formatter.format(Date(it)) }
.orEmpty() .orEmpty()
} }
val lineChart = lineChart( val lineChart = lineChart()
spacing = 110.dp
)
Box(modifier = Modifier.padding(8.dp)) {
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 -> { is ProgressState.Indeterminate -> {
Box(modifier = Modifier.padding(8.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f),
){
CircularProgressIndicator( CircularProgressIndicator(
strokeCap = StrokeCap.Round, strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} }
is ProgressState.Progress -> {
}
}
is ProgressState.Progress -> Box(modifier = Modifier.padding(8.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f),
){
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 {

View File

@ -0,0 +1,381 @@
package llc.arma.ble.app.ui.screen.thermometer.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import llc.arma.ble.R
import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
import llc.arma.ble.app.ui.screen.thermometer.localizedName
@Composable
fun Write(
state: ThermometerContract.State.Display.WriteState,
onEvent: (ThermometerContract.Event) -> Unit
) {
val scope = rememberCoroutineScope()
Column(
modifier = Modifier.animateContentSize { initialValue, targetValue -> }
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is ThermometerContract.State.Display.WriteState.DisplayPreview -> {
state.writeRequest.tx?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it.localizedName} db"
)
}
}
}
}
state.writeRequest.saveHistory?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it.localizedName}"
)
}
}
}
}
state.writeRequest.historyInterval?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал измерний"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it / 1000 / 60 / 60} ч."
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(ThermometerContract.Event.OnWriteBle)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Записать"
)
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
is ThermometerContract.State.Display.WriteState.Writing -> {
Box {
Column() {
Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(48.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}
ThermometerContract.State.Display.WriteState.Success -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_done),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
ThermometerContract.State.Display.WriteState.Failure -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_error),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
}
}
}

View File

@ -23,12 +23,24 @@ import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.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,16 +87,13 @@ 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 {
val bleCallback = object : ScanCallback() { val bleCallback = object : ScanCallback() {
@ -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 deviceCache[serial]?.device?.let {
},
onFailure = { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
emit(Result.failure(it)) app,
return@flow Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
gatt = it.connectGatt(app, false, ReadHistoryCallback(app) {
CoroutineScope(Dispatchers.IO).launch {
send(it)
} }
).let { device -> })
emit(Result.success(ProgressState.Indeterminate))
writeCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
writeData = byteArrayOf(2)
)
val countDataArray = readCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
).fold(
onFailure = {
emit(Result.failure(it))
return@flow
},
onSuccess = { return@fold it }
)
writeCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
writeData = mutableListOf(
1.toByte(),
0.toByte(),
0.toByte()
).apply {
addAll(countDataArray.toList())
}.toByteArray()
)
val firstPackageResponse = readCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
).fold(
onFailure = {
emit(Result.failure(it))
return@flow
},
onSuccess = { return@fold it }
)
if(firstPackageResponse[0] == 250.toByte()){
val interval = firstPackageResponse.getUIntAt(2).toLong()
val lastMeasureTime = firstPackageResponse.getUIntAt(6).toLong()
val realTime = firstPackageResponse.getUIntAt(10).toLong()
val lastMeasureSystemTime = System.currentTimeMillis() - ((realTime - lastMeasureTime) / 10_000)
var temperatureDataArray = firstPackageResponse.asList().subList(14, firstPackageResponse.size)
val temperaturePackage = temperatureDataArray.chunked(2).map {
(it[0] + it[1] * 256).toFloat() / 100f
}.toMutableList()
var dataCount = firstPackageResponse[1].toUByte()
val totalDataSize = dataCount.toInt() + temperaturePackage.size
emit(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
delay(100)
emit(Result.success(ProgressState.Progress(dataCount.toFloat() / totalDataSize.toFloat())))
while(dataCount != 0.toUByte()){
writeCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
writeData = byteArrayOf(5)
)
val readResponse = readCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
).fold(
onFailure = {
emit(Result.failure(it))
return@flow
},
onSuccess = { return@fold it }
)
if(readResponse[0] == 251.toByte()) {
dataCount = readResponse[1].toUByte()
temperatureDataArray = readResponse.toList().subList(2, readResponse.size)
temperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
(it[0] + it[1] * 256).toFloat() / 100f
}
)
emit(Result.success(ProgressState.Progress(totalDataSize.toFloat() / temperaturePackage.size.toFloat())))
} else { } else {
emit(Result.failure(BleException.UnexpectedResponse)) CoroutineScope(Dispatchers.IO).launch {
send(Result.failure(BleException.PermissionDenied))
}
return@callbackFlow
} }
} }
readCharacteristic( awaitClose {
device = device, gatt?.close()
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))
} }
} }
}
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)
} }
override suspend fun writeBle(
serial: String,
request: Ble.Beacon.WriteRequest
): Result<Unit, BleException> {
deviceCache[serial]?.let { result ->
request.tx?.let { writeTx(result.device, it) }?.onFailure {
return Result.failure(it)
} }
private suspend fun writeThermometer(ble: Ble.Thermometer){ writeToFlash(serial).onFailure {
return Result.failure(it)
}
deviceCache[ble.info.serial]?.device?.let { deviceCache.remove(serial)
resultList.remove(serial)
writeTx(it, ble.state.tx)
writeSaveInterval(it, ble.thermometerState.historyInterval)
} }
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,20 +546,32 @@ class BleRepositoryImpl @Inject constructor(
Log.d("read", "onConnectionStateChange $newState $status") Log.d("read", "onConnectionStateChange $newState $status")
if(status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
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.discoverServices() gatt.discoverServices()
} else { } else {
it.resume(Result.failure(BleException.PermissionDenied)) it.resume(Result.failure(BleException.PermissionDenied))
} }
} }
} else {
it.resume(Result.failure(BleException.PermissionDenied))
}
} }
override fun onServicesDiscovered( override fun onServicesDiscovered(
@ -633,20 +590,50 @@ 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(
@ -659,21 +646,26 @@ class BleRepositoryImpl @Inject constructor(
Log.d("read", "onCharacteristicRead $status") Log.d("read", "onCharacteristicRead $status")
if (ActivityCompat.checkSelfPermission( if(status == BluetoothGatt.GATT_SUCCESS) {
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
) { ) {
it.resume(Result.failure(BleException.PermissionDenied))
}else {
gatt.close() gatt.close()
result = value result = value
if(result != null){
it.resume(Result.success(result!!)) it.resume(Result.success(result!!))
} else { } else {
bleGatt?.close() it.resume(Result.failure(BleException.PermissionDenied))
}
} else {
it.resume(Result.failure(BleException.UnexpectedResponse)) it.resume(Result.failure(BleException.UnexpectedResponse))
}
} }
@ -681,14 +673,13 @@ class BleRepositoryImpl @Inject constructor(
} }
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,24 +701,36 @@ class BleRepositoryImpl @Inject constructor(
newState: Int newState: Int
) { ) {
Log.d("write", "onConnectionStateChange $newState") Log.d("write", "onConnectionStateChange $status $newState")
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
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.discoverServices() gatt.discoverServices()
} else {
it.resume(Result.failure(BleException.PermissionDenied))
} }
} else { } else {
if(newState == BluetoothProfile.STATE_DISCONNECTED && status == BluetoothGatt.GATT_FAILURE){ it.resume(Result.failure(BleException.UnexpectedResponse))
bleGatt?.close() bleGatt?.close()
} }
} else {
it.resume(Result.failure(BleException.UnexpectedResponse))
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,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
return
} else {
gatt.close()
it.resume(Unit)
}
}
}
bleGatt = device.connectGatt(app, true, callback)
}
private suspend fun findDeviceBySerial(serial: String): Result<BluetoothDevice, BleException> = suspendCancellableCoroutine {
val bleCallback = object : ScanCallback() {
override fun onScanResult(
callbackType: Int,
result: ScanResult
) {
super.onScanResult(callbackType, result)
if(it.isActive) {
if (ActivityCompat.checkSelfPermission(
app, app,
Manifest.permission.BLUETOOTH_CONNECT Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
gatt.close()
it.resume(Result.success(result.device)) if(status == BluetoothGatt.GATT_SUCCESS) {
it.resume(Result.success(Unit))
}else{ }else{
CoroutineScope(Dispatchers.IO).launch {
it.resume( it.resume(Result.failure(BleException.UnexpectedResponse))
Result.failure(BleException.PermissionDenied)
)
}
} }
} else {
it.resume(Result.failure(BleException.PermissionDenied))
} }
} }
} }
val bleScanner = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app.getSystemService(BluetoothManager::class.java).adapter.bluetoothLeScanner app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED) {
bleScanner.startScan( bleGatt = device.connectGatt(app, false, callback)
listOf(ScanFilter.Builder().setDeviceAddress(serial).build()),
ScanSettings.Builder() } else {
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) it.resume(Result.failure(BleException.PermissionDenied))
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
.setReportDelay(400L)
.build(),
bleCallback)
it.invokeOnCancellation {
bleScanner.stopScan(bleCallback)
} }
} }
} }
fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.value = data
writeCharacteristic(characteristic)
}
}

View File

@ -0,0 +1,292 @@
package llc.arma.ble.data
import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import java.util.*
enum class Property {
DATA_SIZE, PACKAGE
}
class ReadHistoryCallback(
private val app: Application,
private val onResult: (Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>) -> Unit
) : BluetoothGattCallback() {
private fun ByteArray.getUIntAt(idx: Int) =
((this[idx + 3].toUInt() and 0xFFu) shl 24) or
((this[idx + 2].toUInt() and 0xFFu) shl 16) or
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
private var readProperty: Property? = null
init {
onResult(Result.success(ProgressState.Indeterminate))
}
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(temperatureHistoryReadUUID)?.let {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
readProperty = Property.DATA_SIZE
gatt.writeCharacteristic(it, byteArrayOf(2))
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
}
}
}
private var lastMeasureSystemTime: Long? = null
private var bleMeasureInterval: Long? = null
private var bleRealTime: Long? = null
private var bleLastMeasureTime: Long? = null
private val resultTemperaturePackage: MutableList<Float> = mutableListOf()
var expectedDataSize: Int? = null
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, status)
onCommonCharacteristicRead(gatt, characteristic, characteristic.value, status)
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, value, status)
//onCommonCharacteristicRead(gatt, characteristic, value, status)
}
private fun onCommonCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
){
if(status == BluetoothGatt.GATT_SUCCESS){
when(readProperty){
Property.DATA_SIZE -> {
val writeData = mutableListOf(
1.toByte(),
0.toByte(),
0.toByte()
).apply {
addAll(value.toList())
}.toByteArray()
readProperty = Property.PACKAGE
gatt.writeCharacteristic(characteristic, writeData)
}
Property.PACKAGE -> {
if(value[0] == 250.toByte()){
bleMeasureInterval = value.getUIntAt(2).toLong()
bleLastMeasureTime = value.getUIntAt(6).toLong()
bleRealTime = value.getUIntAt(10).toLong()
lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) / 10_000)
val temperatureDataArray = value.asList().subList(14, value.size)
resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
(it[0] + it[1] * 256).toFloat() / 100f
}.toMutableList()
)
val totalDataSize = value[1].toUByte().toInt() + temperatureDataArray.size / 2
val nextPackageDataCount = value[1].toUByte()
expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
onResult(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
onResult(Result.success(ProgressState.Progress(nextPackageDataCount.toFloat() / totalDataSize.toFloat())))
if(nextPackageDataCount != 0.toUByte()){
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
gatt.writeCharacteristic(characteristic, byteArrayOf(5))
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(
Result.success(
ProgressState.Finished(
resultTemperaturePackage.withIndex().map {
Ble.Thermometer.MeasurePoint(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
value = it.value
)
}
)
)
)
gatt.close()
}
}
if(value[0] == 251.toByte()) {
val nextPackageDataCount = value[1].toUByte()
val temperatureDataArray = value.toList().subList(2, value.size)
resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
(it[0] + it[1] * 256).toFloat() / 100f
}
)
onResult(Result.success(ProgressState.Progress(expectedDataSize!!.toFloat() / resultTemperaturePackage.size.toFloat())))
if(nextPackageDataCount != 0.toUByte()){
val writeData = byteArrayOf(5)
gatt.writeCharacteristic(characteristic, writeData)
gatt.readCharacteristic(characteristic)
} else {
onResult(
Result.success(
ProgressState.Finished(
resultTemperaturePackage.withIndex().map {
Ble.Thermometer.MeasurePoint(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
value = it.value
)
}
)
)
)
gatt.close()
}
}
}
else -> {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if(status == BluetoothGatt.GATT_SUCCESS){
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}

View File

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

View File

@ -0,0 +1,18 @@
package llc.arma.ble.domain.usecase
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject
class ChangeBlePassword @Inject constructor(
private val bleRepository: BleRepository
) {
suspend operator fun invoke(
password: String,
serial: String
): llc.arma.ble.domain.Result<Unit, BleException> {
return bleRepository.changeBlePassword(password, serial)
}
}

View File

@ -1,6 +1,7 @@
package llc.arma.ble.domain.usecase 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)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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>

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

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

View File

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

View File

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