rotations save
This commit is contained in:
parent
55b6807081
commit
549b625820
|
|
@ -96,6 +96,10 @@ dependencies {
|
||||||
implementation "com.patrykandpatrick.vico:compose:1.7.1"
|
implementation "com.patrykandpatrick.vico:compose:1.7.1"
|
||||||
implementation "com.patrykandpatrick.vico:compose-m3:1.7.1"
|
implementation "com.patrykandpatrick.vico:compose-m3:1.7.1"
|
||||||
|
|
||||||
|
implementation "androidx.room:room-runtime:2.6.1"
|
||||||
|
kapt("androidx.room:room-compiler:2.6.1")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
|
||||||
implementation files('libs/poishadow-all.jar')
|
implementation files('libs/poishadow-all.jar')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package llc.arma.ble.app.framework.di
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import llc.arma.ble.data.db.AppDatabase
|
||||||
|
import llc.arma.ble.data.db.RotationsDao
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class DatabaseModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDatabase(app: Application): AppDatabase {
|
||||||
|
return Room.databaseBuilder(app, AppDatabase::class.java, "app-database").build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRotationsDao(db: AppDatabase): RotationsDao {
|
||||||
|
return db.getRotationsDao()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,13 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import llc.arma.ble.data.BleRepositoryImpl
|
import llc.arma.ble.data.repository.BleRepositoryImpl
|
||||||
import llc.arma.ble.data.EmailRepositoryImpl
|
import llc.arma.ble.data.repository.EmailRepositoryImpl
|
||||||
import llc.arma.ble.data.XlsxRepositoryImpl
|
import llc.arma.ble.data.repository.RotationsRepositoryImpl
|
||||||
|
import llc.arma.ble.data.repository.XlsxRepositoryImpl
|
||||||
import llc.arma.ble.domain.repository.BleRepository
|
import llc.arma.ble.domain.repository.BleRepository
|
||||||
import llc.arma.ble.domain.repository.EmailRepository
|
import llc.arma.ble.domain.repository.EmailRepository
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
import llc.arma.ble.domain.repository.XlsxRepository
|
import llc.arma.ble.domain.repository.XlsxRepository
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -21,6 +23,9 @@ interface RepositoryBinding {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindEmailRepository(repository: EmailRepositoryImpl): EmailRepository
|
fun bindEmailRepository(repository: EmailRepositoryImpl): EmailRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindRotationsRepository(repository: RotationsRepositoryImpl): RotationsRepository
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository
|
fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,19 @@ import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.SurfaceView
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
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.Button
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.ModalBottomSheetLayout
|
import androidx.compose.material.ModalBottomSheetLayout
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
|
|
@ -28,8 +32,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.app.ActivityCompat.startActivityForResult
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
|
@ -231,19 +234,20 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) {
|
fun Camera(){
|
||||||
val eventHandler = rememberUpdatedState(onEvent)
|
|
||||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner.value) {
|
AndroidView(
|
||||||
val lifecycle = lifecycleOwner.value.lifecycle
|
factory = {
|
||||||
val observer = LifecycleEventObserver { owner, event ->
|
val cameraManager = it.getSystemService(CameraManager::class.java)
|
||||||
eventHandler.value(owner, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycle.addObserver(observer)
|
cameraManager.cameraIdList.forEach {
|
||||||
onDispose {
|
|
||||||
lifecycle.removeObserver(observer)
|
println("$it is front: ${cameraManager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT}")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return@AndroidView SurfaceView(it)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +66,10 @@ class ConnectionContract {
|
||||||
val serial: String
|
val serial: String
|
||||||
) : Navigation()
|
) : Navigation()
|
||||||
|
|
||||||
|
data class NavigateToRotationsStatistic(
|
||||||
|
val serial: String
|
||||||
|
) : Navigation()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class InnerNavigation : Effect() {
|
sealed class InnerNavigation : Effect() {
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,9 @@ fun ConnectionScreen(
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
innerScreen = null
|
innerScreen = null
|
||||||
}
|
}
|
||||||
)
|
){
|
||||||
|
onNavigationEvent(ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic(it.ble.serial))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.rounded.CloudUpload
|
import androidx.compose.material.icons.rounded.CloudUpload
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
|
import androidx.compose.material.icons.rounded.TableView
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
|
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
|
||||||
|
|
@ -71,6 +72,7 @@ class AccelEntry(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccelerometerHistory(
|
fun AccelerometerHistory(
|
||||||
ble: BleInfo,
|
ble: BleInfo,
|
||||||
|
|
@ -79,7 +81,8 @@ fun AccelerometerHistory(
|
||||||
fftAxis: FftAxis,
|
fftAxis: FftAxis,
|
||||||
fftMode: FftViewMode,
|
fftMode: FftViewMode,
|
||||||
frequency: FftFrequency,
|
frequency: FftFrequency,
|
||||||
onDismiss: (() -> Unit)? = null
|
onDismiss: (() -> Unit)? = null,
|
||||||
|
onShowStatistic: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
|
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
|
||||||
|
|
@ -95,74 +98,85 @@ fun AccelerometerHistory(
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
Column(
|
Column() {
|
||||||
modifier = Modifier.fillMaxHeight(0.9f)
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(
|
TopAppBar(
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
navigationIcon = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onDismiss?.let {
|
||||||
) {
|
|
||||||
|
|
||||||
onDismiss?.let {
|
IconButton(onClick = it) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
IconButton(onClick = it) {
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
val title = when(state){
|
||||||
|
is AccelerometerHistoryContract.State.Display -> {
|
||||||
|
when (state.loadingHistoryState) {
|
||||||
|
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})"
|
||||||
|
is ProgressState.Indeterminate -> accelMode.localized
|
||||||
|
is ProgressState.Progress -> accelMode.localized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccelerometerHistoryContract.State.Exception -> accelMode.localized
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onShowStatistic,
|
||||||
|
enabled = when(state){
|
||||||
|
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
||||||
|
AccelerometerHistoryContract.State.Exception -> false
|
||||||
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.ArrowBack,
|
imageVector = Icons.Rounded.TableView,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
IconButton(
|
||||||
|
onClick = {
|
||||||
val title = when(state){
|
viewModel.setEvent(AccelerometerHistoryContract.Event.OnExport)
|
||||||
is AccelerometerHistoryContract.State.Display -> {
|
},
|
||||||
when (state.loadingHistoryState) {
|
enabled = when(state){
|
||||||
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})"
|
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
||||||
is ProgressState.Indeterminate -> accelMode.localized
|
AccelerometerHistoryContract.State.Exception -> false
|
||||||
is ProgressState.Progress -> accelMode.localized
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.CloudUpload,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
AccelerometerHistoryContract.State.Exception -> accelMode.localized
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
IconButton(
|
||||||
modifier = Modifier.weight(1f),
|
onClick = {
|
||||||
text = title,
|
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
|
||||||
style = MaterialTheme.typography.titleLarge
|
},
|
||||||
)
|
enabled = when(state){
|
||||||
|
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
||||||
IconButton(
|
AccelerometerHistoryContract.State.Exception -> true
|
||||||
onClick = {
|
}
|
||||||
viewModel.setEvent(AccelerometerHistoryContract.Event.OnExport)
|
) {
|
||||||
},
|
Icon(
|
||||||
enabled = when(state){
|
imageVector = Icons.Rounded.Refresh,
|
||||||
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
contentDescription = null
|
||||||
AccelerometerHistoryContract.State.Exception -> false
|
)
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.CloudUpload,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
|
|
||||||
},
|
|
||||||
enabled = when(state){
|
|
||||||
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
|
|
||||||
AccelerometerHistoryContract.State.Exception -> true
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Refresh,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ 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.ChangePasswordContract
|
||||||
import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen
|
import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen
|
||||||
|
import llc.arma.ble.app.ui.screen.rotation.delete.RotationDeleteContract
|
||||||
|
import llc.arma.ble.app.ui.screen.rotation.delete.RotationDeleteScreen
|
||||||
|
import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticContract
|
||||||
|
import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen() {
|
fun MainScreen() {
|
||||||
|
|
@ -49,7 +53,10 @@ 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}")
|
is ConnectionContract.Effect.Navigation.NavigateToChangePassword ->
|
||||||
|
controller.navigate("change_password/${it.serial}")
|
||||||
|
is ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic ->
|
||||||
|
controller.navigate("rotation_statistic/${it.serial}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -57,6 +64,36 @@ fun MainScreen() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "rotation_statistic/{serial}",
|
||||||
|
content = {
|
||||||
|
|
||||||
|
RotationStatisticScreen {
|
||||||
|
when(it){
|
||||||
|
is RotationStatisticContract.Effect.Navigation.NavigateToDelete -> {
|
||||||
|
controller.navigate("rotation_delete/${it.serial}")
|
||||||
|
}
|
||||||
|
RotationStatisticContract.Effect.Navigation.NavigateUp -> {
|
||||||
|
controller.navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog(
|
||||||
|
route = "rotation_delete/{serial}",
|
||||||
|
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
content = {
|
||||||
|
RotationDeleteScreen {
|
||||||
|
when(it){
|
||||||
|
RotationDeleteContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dialog(
|
dialog(
|
||||||
route = "change_password/{serial}",
|
route = "change_password/{serial}",
|
||||||
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
|
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package llc.arma.ble.app.ui.screen.rotation.delete
|
||||||
|
|
||||||
|
import llc.arma.ble.app.ui.common.ViewEvent
|
||||||
|
import llc.arma.ble.app.ui.common.ViewSideEffect
|
||||||
|
import llc.arma.ble.app.ui.common.ViewState
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
|
|
||||||
|
class RotationDeleteContract {
|
||||||
|
|
||||||
|
sealed class Event : ViewEvent {
|
||||||
|
|
||||||
|
object OnNavigateUp : Event()
|
||||||
|
|
||||||
|
object OnDelete : Event()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object State : ViewState
|
||||||
|
|
||||||
|
sealed class Effect : ViewSideEffect {
|
||||||
|
|
||||||
|
sealed class Navigation : Effect(){
|
||||||
|
|
||||||
|
object NavigateUp : Navigation()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package llc.arma.ble.app.ui.screen.rotation.delete
|
||||||
|
|
||||||
|
import android.icu.text.SimpleDateFormat
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticContract
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RotationDeleteScreen(
|
||||||
|
onNavigation: (RotationDeleteContract.Effect.Navigation) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val viewModel = hiltViewModel<RotationDeleteViewModel>()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.effect.onEach {
|
||||||
|
when(it){
|
||||||
|
is RotationDeleteContract.Effect.Navigation -> onNavigation(it)
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
text = "Удалить сохраненные записи?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEvent(
|
||||||
|
RotationDeleteContract.Event.OnNavigateUp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Отмена")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEvent(
|
||||||
|
RotationDeleteContract.Event.OnDelete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Удалить")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package llc.arma.ble.app.ui.screen.rotation.delete
|
||||||
|
|
||||||
|
import android.icu.util.GregorianCalendar
|
||||||
|
import android.util.Log
|
||||||
|
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.DeleteRotationsBySerial
|
||||||
|
import llc.arma.ble.domain.usecase.GetRotationsBySerial
|
||||||
|
import llc.arma.ble.domain.usecase.GetWheelRadiusBySerial
|
||||||
|
import llc.arma.ble.domain.usecase.SetWheelRadius
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class RotationDeleteViewModel @Inject constructor(
|
||||||
|
private val savedStateHandle: SavedStateHandle,
|
||||||
|
private val deleteRotationsBySerial: DeleteRotationsBySerial
|
||||||
|
) : BaseViewModel<RotationDeleteContract.State, RotationDeleteContract.Event, RotationDeleteContract.Effect>() {
|
||||||
|
|
||||||
|
override fun setInitialState() = RotationDeleteContract.State
|
||||||
|
|
||||||
|
override fun handleEvents(event: RotationDeleteContract.Event) {
|
||||||
|
when(event){
|
||||||
|
is RotationDeleteContract.Event.OnDelete -> reduce(viewState.value, event)
|
||||||
|
is RotationDeleteContract.Event.OnNavigateUp -> reduce(viewState.value, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationDeleteContract.State,
|
||||||
|
event: RotationDeleteContract.Event.OnNavigateUp
|
||||||
|
) {
|
||||||
|
|
||||||
|
setEffect {
|
||||||
|
|
||||||
|
RotationDeleteContract.Effect.Navigation.NavigateUp
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationDeleteContract.State,
|
||||||
|
event: RotationDeleteContract.Event.OnDelete
|
||||||
|
) {
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
deleteRotationsBySerial(savedStateHandle.get<String>("serial")!!)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setEffect {
|
||||||
|
|
||||||
|
RotationDeleteContract.Effect.Navigation.NavigateUp
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package llc.arma.ble.app.ui.screen.rotation.statistic
|
||||||
|
|
||||||
|
import llc.arma.ble.app.ui.common.ViewEvent
|
||||||
|
import llc.arma.ble.app.ui.common.ViewSideEffect
|
||||||
|
import llc.arma.ble.app.ui.common.ViewState
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
|
|
||||||
|
class RotationStatisticContract {
|
||||||
|
|
||||||
|
sealed class Event : ViewEvent {
|
||||||
|
|
||||||
|
object OnNavigateUp : Event()
|
||||||
|
|
||||||
|
object OnNavigateToDelete : Event()
|
||||||
|
|
||||||
|
data class OnRadiusChanged(
|
||||||
|
val radius: Long?
|
||||||
|
): Event()
|
||||||
|
|
||||||
|
data class OnStartDateChanged(
|
||||||
|
val start: Long?
|
||||||
|
): Event()
|
||||||
|
|
||||||
|
data class OnStopDateChanged(
|
||||||
|
val stop: Long?
|
||||||
|
): Event()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class State : ViewState {
|
||||||
|
|
||||||
|
object Loading : State()
|
||||||
|
|
||||||
|
data class Display(
|
||||||
|
val rotations: List<Rotation>,
|
||||||
|
val ranges: List<RotationRange>,
|
||||||
|
val radius: Long?,
|
||||||
|
val startDate: Long?,
|
||||||
|
val endDate: Long?
|
||||||
|
) : State() {
|
||||||
|
|
||||||
|
data class RotationRange(
|
||||||
|
val distance: Double,
|
||||||
|
val startDate: Long,
|
||||||
|
val endDate: Long,
|
||||||
|
val maxSpeed: Double,
|
||||||
|
val avgSpeed: Double,
|
||||||
|
val rotations: Float,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Effect : ViewSideEffect {
|
||||||
|
|
||||||
|
sealed class Navigation : Effect(){
|
||||||
|
|
||||||
|
object NavigateUp : Navigation()
|
||||||
|
|
||||||
|
data class NavigateToDelete(
|
||||||
|
val serial: String
|
||||||
|
) : Navigation()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,496 @@
|
||||||
|
package llc.arma.ble.app.ui.screen.rotation.statistic
|
||||||
|
|
||||||
|
import android.icu.text.SimpleDateFormat
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowForward
|
||||||
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RotationStatisticScreen(
|
||||||
|
onNavigation: (RotationStatisticContract.Effect.Navigation) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val viewModel = hiltViewModel<RotationStatisticViewModel>()
|
||||||
|
val state = viewModel.viewState.value
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.effect.onEach {
|
||||||
|
when(it){
|
||||||
|
is RotationStatisticContract.Effect.Navigation -> onNavigation(it)
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEvent(RotationStatisticContract.Event.OnNavigateUp)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = "Обороты",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEvent(RotationStatisticContract.Event.OnNavigateToDelete)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Delete,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.padding(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
if(state is RotationStatisticContract.State.Display){
|
||||||
|
|
||||||
|
var showStartDateSelector by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showEndDateSelector by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.width(IntrinsicSize.Max)
|
||||||
|
.height(IntrinsicSize.Max)
|
||||||
|
) {
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
readOnly = true,
|
||||||
|
value = dateFormat.format(Date(state.startDate ?: state.ranges.minOfOrNull { it.startDate } ?: 0)),
|
||||||
|
onValueChange = {},
|
||||||
|
label = {
|
||||||
|
Text(text = "Начало")
|
||||||
|
},
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable { showStartDateSelector = true }
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.width(IntrinsicSize.Max)
|
||||||
|
.height(IntrinsicSize.Max)
|
||||||
|
) {
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
readOnly = true,
|
||||||
|
value = dateFormat.format(Date(state.endDate ?: state.ranges.maxOfOrNull { it.endDate } ?: 0)),
|
||||||
|
onValueChange = {},
|
||||||
|
label = {
|
||||||
|
Text(text = "Завершение")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable { showEndDateSelector = true }
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
singleLine = true,
|
||||||
|
value = state.radius?.toString() ?: "",
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.setEvent(
|
||||||
|
RotationStatisticContract.Event.OnRadiusChanged(
|
||||||
|
it.filter { it.isDigit() }.toLongOrNull()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(text = "Радиус колеса (мм.)")
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
Text(text = "Пройденное расстояние")
|
||||||
|
Text(text = "Движение вперед")
|
||||||
|
Text(text = "Движение назад")
|
||||||
|
Text(text = "Стоянка")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
val duration = state.ranges.filter { it.distance == 0.0 }.sumOf { it.endDate - it.startDate }
|
||||||
|
val hours = duration / TimeUnit.HOURS.toMillis(1)
|
||||||
|
val minutes = (duration - (hours * TimeUnit.HOURS.toMillis(1))) / TimeUnit.MINUTES.toMillis(1)
|
||||||
|
val seconds = (duration - (hours * TimeUnit.HOURS.toMillis(1)) - (minutes * TimeUnit.MINUTES.toMillis(1))) / TimeUnit.SECONDS.toMillis(1)
|
||||||
|
|
||||||
|
Text(text = state.ranges.sumOf { it.distance.absoluteValue }.roundTo().toString() + " м.")
|
||||||
|
Text(text = state.ranges.filter { it.distance > 0 }.sumOf { it.distance.absoluteValue }.roundTo().toString() + " м.")
|
||||||
|
Text(text = state.ranges.filter { it.distance < 0 }.sumOf { it.distance.absoluteValue }.roundTo().toString() + " м.")
|
||||||
|
Text(text = "${hours}ч ${minutes}м. ${seconds}с.")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var reversed by remember {
|
||||||
|
mutableStateOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(text = "Порядок")
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { reversed = reversed.not() }
|
||||||
|
) {
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = if (reversed) Icons.Rounded.ArrowBack else Icons.Rounded.ArrowForward,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
items(items = state.ranges.let { if (reversed) it.reversed() else it}){
|
||||||
|
DataRange(range = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showStartDateSelector) {
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
onDismissRequest = {
|
||||||
|
showEndDateSelector = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = state.startDate
|
||||||
|
)
|
||||||
|
|
||||||
|
Column{
|
||||||
|
|
||||||
|
DatePicker(state = datePickerState)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showStartDateSelector = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Отмена")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEvent(
|
||||||
|
RotationStatisticContract.Event.OnStartDateChanged(
|
||||||
|
datePickerState.selectedDateMillis ?: state.startDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
showStartDateSelector = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "ок")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showEndDateSelector) {
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
onDismissRequest = {
|
||||||
|
showEndDateSelector = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = state.endDate
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
DatePicker(state = datePickerState)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showEndDateSelector = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Отмена")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.setEvent(
|
||||||
|
RotationStatisticContract.Event.OnStopDateChanged(
|
||||||
|
datePickerState.selectedDateMillis ?: state.endDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
showEndDateSelector = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "ок")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Double.roundTo(numFractionDigits: Int = 2): Double {
|
||||||
|
val factor = 10.0.pow(numFractionDigits.toDouble())
|
||||||
|
return (this * factor).roundToInt() / factor
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Double.round(decimals: Int = 2): Double = "%.${decimals}f".format(this).toDouble()
|
||||||
|
|
||||||
|
private val dateTimeFormat = SimpleDateFormat("dd.MM HH:mm:ss")
|
||||||
|
private val dateFormat = SimpleDateFormat("dd.MM HH:mm:ss")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DataRange(range: RotationStatisticContract.State.Display.RotationRange){
|
||||||
|
|
||||||
|
Card {
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(12.dp)
|
||||||
|
.width(IntrinsicSize.Max)
|
||||||
|
) {
|
||||||
|
|
||||||
|
val duration = range.endDate - range.startDate
|
||||||
|
val hours = duration / TimeUnit.HOURS.toMillis(1)
|
||||||
|
val minutes = (duration - (hours * TimeUnit.HOURS.toMillis(1))) / TimeUnit.MINUTES.toMillis(1)
|
||||||
|
val seconds = (duration - (hours * TimeUnit.HOURS.toMillis(1)) - (minutes * TimeUnit.MINUTES.toMillis(1))) / TimeUnit.SECONDS.toMillis(1)
|
||||||
|
|
||||||
|
val directionText = if(range.distance > 0){
|
||||||
|
"-->"
|
||||||
|
}else{
|
||||||
|
if(range.distance < 0) {
|
||||||
|
"<--"
|
||||||
|
} else {
|
||||||
|
"<<P>>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
Text(text = "Начало")
|
||||||
|
Text(text = "Конец")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
Text(text = dateTimeFormat.format(Date(range.startDate)))
|
||||||
|
Text(text = dateTimeFormat.format(Date(range.endDate)))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text = "Направление: " + directionText)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
Text(text = "Время")
|
||||||
|
|
||||||
|
if(range.distance != 0.0) {
|
||||||
|
|
||||||
|
Text(text = "Расстояние")
|
||||||
|
Text(text = "Vmax.")
|
||||||
|
Text(text = "Vср.")
|
||||||
|
Text(text = "Обороты")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
Text(text = "${hours}ч ${minutes}м. ${seconds}с.")
|
||||||
|
|
||||||
|
if(range.distance != 0.0) {
|
||||||
|
|
||||||
|
Text(text = range.distance.absoluteValue.roundTo().toString() + " м.")
|
||||||
|
Text(text = range.maxSpeed.roundTo().toString() + " Км/ч")
|
||||||
|
Text(text = range.avgSpeed.roundTo().toString() + " Км/ч")
|
||||||
|
Text(text = range.rotations.absoluteValue.toDouble().roundTo().toString())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
package llc.arma.ble.app.ui.screen.rotation.statistic
|
||||||
|
|
||||||
|
import android.icu.util.GregorianCalendar
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.patrykandpatrick.vico.core.extension.sumOf
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import llc.arma.ble.app.ui.common.BaseViewModel
|
||||||
|
import llc.arma.ble.domain.usecase.GetRotationsBySerial
|
||||||
|
import llc.arma.ble.domain.usecase.GetWheelRadiusBySerial
|
||||||
|
import llc.arma.ble.domain.usecase.SetWheelRadius
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
fun <T> Collection<T>.splitBy(separator: (T) -> Any?): List<List<T>> {
|
||||||
|
|
||||||
|
val result = mutableListOf<MutableList<T>>()
|
||||||
|
|
||||||
|
var newSublist = true
|
||||||
|
for (item in this) {
|
||||||
|
newSublist = (separator(item) != result.lastOrNull()?.lastOrNull()?.let { separator(it) })
|
||||||
|
if (newSublist || result.lastOrNull() == null)
|
||||||
|
result += mutableListOf<T>()
|
||||||
|
result.last() += item
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class RotationStatisticViewModel @Inject constructor(
|
||||||
|
private val setWheelRadius: SetWheelRadius,
|
||||||
|
private val getWheelRadius: GetWheelRadiusBySerial,
|
||||||
|
private val getRotationsBySerial: GetRotationsBySerial,
|
||||||
|
private val savedStateHandle: SavedStateHandle
|
||||||
|
) : BaseViewModel<RotationStatisticContract.State, RotationStatisticContract.Event, RotationStatisticContract.Effect>() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
load()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setInitialState() = RotationStatisticContract.State.Loading
|
||||||
|
|
||||||
|
override fun handleEvents(event: RotationStatisticContract.Event) {
|
||||||
|
when(event){
|
||||||
|
is RotationStatisticContract.Event.OnRadiusChanged -> reduce(viewState.value, event)
|
||||||
|
is RotationStatisticContract.Event.OnStartDateChanged -> reduce(viewState.value, event)
|
||||||
|
is RotationStatisticContract.Event.OnStopDateChanged -> reduce(viewState.value, event)
|
||||||
|
is RotationStatisticContract.Event.OnNavigateToDelete -> reduce(viewState.value, event)
|
||||||
|
is RotationStatisticContract.Event.OnNavigateUp -> reduce(viewState.value, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun load(){
|
||||||
|
savedStateHandle.get<String>("serial")?.let {
|
||||||
|
|
||||||
|
val state = viewState.value
|
||||||
|
|
||||||
|
val r = getWheelRadius(it)?.toFloat() ?: 500f
|
||||||
|
val rCm = r / 10f
|
||||||
|
val circumference = 2 * Math.PI * rCm
|
||||||
|
Log.d("circumference", circumference.toString())
|
||||||
|
|
||||||
|
var rotations = getRotationsBySerial(it)
|
||||||
|
|
||||||
|
if(state is RotationStatisticContract.State.Display){
|
||||||
|
rotations = rotations.filter {
|
||||||
|
it.date >= (state.startDate ?: 0) && it.date <= (state.endDate ?: Long.MAX_VALUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val split: List<RotationStatisticContract.State.Display.RotationRange> = if(rotations.size < 2) {
|
||||||
|
emptyList()
|
||||||
|
}else {
|
||||||
|
|
||||||
|
val pointDuration = rotations[1].date - rotations[0].date
|
||||||
|
|
||||||
|
rotations.splitBy {
|
||||||
|
Log.d("rDate", it.date.toString())
|
||||||
|
if (it.rotations > 0) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
if (it.rotations < 0) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.map { range ->
|
||||||
|
|
||||||
|
val start = range.minOf { it.date } - pointDuration
|
||||||
|
val end = range.maxOf { it.date }
|
||||||
|
|
||||||
|
val speeds = range.map {
|
||||||
|
val km = ((it.rotations.absoluteValue / 8f) * circumference) / 100_000f
|
||||||
|
km / (pointDuration.toFloat() / TimeUnit.HOURS.toMillis(1).toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
RotationStatisticContract.State.Display.RotationRange(
|
||||||
|
distance = ((range.sumOf { it.rotations.toFloat() } / 8f) * circumference) / 100f,
|
||||||
|
startDate = start,
|
||||||
|
endDate = end,
|
||||||
|
maxSpeed = speeds.max(),
|
||||||
|
avgSpeed = speeds.average(),
|
||||||
|
rotations = range.sumOf { it.rotations.toFloat() } / 8f
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
|
||||||
|
when(this){
|
||||||
|
is RotationStatisticContract.State.Display -> {
|
||||||
|
copy(
|
||||||
|
rotations = rotations,
|
||||||
|
ranges = split
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RotationStatisticContract.State.Loading -> {
|
||||||
|
RotationStatisticContract.State.Display(
|
||||||
|
rotations = rotations,
|
||||||
|
ranges = split,
|
||||||
|
radius = r.toLong(),
|
||||||
|
startDate = null,
|
||||||
|
endDate = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} ?: throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationStatisticContract.State,
|
||||||
|
event: RotationStatisticContract.Event.OnRadiusChanged
|
||||||
|
) {
|
||||||
|
|
||||||
|
if(state is RotationStatisticContract.State.Display){
|
||||||
|
|
||||||
|
setState {
|
||||||
|
state.copy(radius = event.radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
setWheelRadius(savedStateHandle.get<String>("serial")!!, event.radius ?: 100L)
|
||||||
|
|
||||||
|
load()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationStatisticContract.State,
|
||||||
|
event: RotationStatisticContract.Event.OnStartDateChanged
|
||||||
|
) {
|
||||||
|
|
||||||
|
if(state is RotationStatisticContract.State.Display){
|
||||||
|
|
||||||
|
val mCalendar = GregorianCalendar()
|
||||||
|
val mTimeZone = mCalendar.timeZone
|
||||||
|
val mGMTOffset: Int = mTimeZone.rawOffset
|
||||||
|
|
||||||
|
val start = event.start?.let {
|
||||||
|
TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(event.start)) - mGMTOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("date", start.toString() + " " + event.start)
|
||||||
|
|
||||||
|
setState {
|
||||||
|
state.copy(startDate = start)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
load()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationStatisticContract.State,
|
||||||
|
event: RotationStatisticContract.Event.OnStopDateChanged
|
||||||
|
) {
|
||||||
|
|
||||||
|
if(state is RotationStatisticContract.State.Display){
|
||||||
|
|
||||||
|
val mCalendar = GregorianCalendar()
|
||||||
|
val mTimeZone = mCalendar.timeZone
|
||||||
|
val mGMTOffset: Int = mTimeZone.rawOffset
|
||||||
|
|
||||||
|
val stop = event.stop?.let {
|
||||||
|
TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(event.stop) + 1) - mGMTOffset - 1_000
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
state.copy(endDate = stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
load()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationStatisticContract.State,
|
||||||
|
event: RotationStatisticContract.Event.OnNavigateUp
|
||||||
|
) {
|
||||||
|
|
||||||
|
setEffect {
|
||||||
|
|
||||||
|
RotationStatisticContract.Effect.Navigation.NavigateUp
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reduce(
|
||||||
|
state: RotationStatisticContract.State,
|
||||||
|
event: RotationStatisticContract.Event.OnNavigateToDelete
|
||||||
|
) {
|
||||||
|
|
||||||
|
setEffect {
|
||||||
|
|
||||||
|
RotationStatisticContract.Effect.Navigation.NavigateToDelete(savedStateHandle.get<String>("serial")!!)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package llc.arma.ble.data.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import llc.arma.ble.data.model.RotationEntity
|
||||||
|
import llc.arma.ble.data.model.WheelEntity
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [RotationEntity::class, WheelEntity::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun getRotationsDao(): RotationsDao
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package llc.arma.ble.data.db
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import llc.arma.ble.data.model.RotationEntity
|
||||||
|
import llc.arma.ble.data.model.WheelEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RotationsDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun save(rotations: WheelEntity)
|
||||||
|
|
||||||
|
@Query("select * from wheel where bleId = :serial")
|
||||||
|
suspend fun getWheelBySerial(serial: String): WheelEntity?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun save(rotations: List<RotationEntity>)
|
||||||
|
|
||||||
|
@Query("delete from rotation where bleId = :serial")
|
||||||
|
suspend fun deleteBySerial(serial: String)
|
||||||
|
|
||||||
|
@Query("delete from rotation where bleId = :serial and date > :date")
|
||||||
|
suspend fun deleteBySerialAndDateMoreThan(serial: String, date: Long)
|
||||||
|
|
||||||
|
@Query("select * from rotation where bleId = :serial order by date asc")
|
||||||
|
suspend fun getBySerialSortedByDate(serial: String): List<RotationEntity>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package llc.arma.ble.data.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "rotation",
|
||||||
|
indices = [Index(unique = true, value = ["bleId", "date"])],
|
||||||
|
primaryKeys = ["bleId", "date"]
|
||||||
|
)
|
||||||
|
class RotationEntity(
|
||||||
|
|
||||||
|
val bleId: String,
|
||||||
|
|
||||||
|
val date: Long,
|
||||||
|
|
||||||
|
val rotations: Long
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package llc.arma.ble.data.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "wheel",
|
||||||
|
indices = [Index(unique = true, value = ["bleId"])]
|
||||||
|
)
|
||||||
|
class WheelEntity(
|
||||||
|
|
||||||
|
@PrimaryKey
|
||||||
|
val bleId: String,
|
||||||
|
|
||||||
|
val radius: Long
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
|
@ -14,12 +14,13 @@ import kotlinx.coroutines.flow.flow
|
||||||
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.data.extensions.checkPermission
|
import llc.arma.ble.data.db.RotationsDao
|
||||||
import llc.arma.ble.data.extensions.fromByte
|
import llc.arma.ble.data.repository.extensions.checkPermission
|
||||||
import llc.arma.ble.data.extensions.get4byteUIntAt
|
import llc.arma.ble.data.repository.extensions.fromByte
|
||||||
import llc.arma.ble.data.extensions.info
|
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
|
||||||
import llc.arma.ble.data.extensions.sendData
|
import llc.arma.ble.data.repository.extensions.info
|
||||||
import llc.arma.ble.data.extensions.toTemperature
|
import llc.arma.ble.data.repository.extensions.sendData
|
||||||
|
import llc.arma.ble.data.repository.extensions.toTemperature
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.common.ProgressState
|
import llc.arma.ble.domain.common.ProgressState
|
||||||
|
|
@ -67,7 +68,8 @@ 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,
|
||||||
|
private val rotationsDao: RotationsDao
|
||||||
) : BleRepository {
|
) : BleRepository {
|
||||||
|
|
||||||
val resultList: MutableMap<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>())
|
val resultList: MutableMap<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>())
|
||||||
|
|
@ -536,13 +538,17 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
request.tx?.let {
|
request.tx?.let {
|
||||||
|
|
||||||
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(txWriteUUID)?.write(
|
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
|
||||||
|
txWriteUUID
|
||||||
|
)?.write(
|
||||||
DataByteArray.from(it.sendData)
|
DataByteArray.from(it.sendData)
|
||||||
) ?: return Result.failure(BleException.UnexpectedResponse)
|
) ?: return Result.failure(BleException.UnexpectedResponse)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(flashWriteUUID)!!.write(
|
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
|
||||||
|
flashWriteUUID
|
||||||
|
)!!.write(
|
||||||
DataByteArray.from(9)
|
DataByteArray.from(9)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -579,6 +585,8 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
(this shr (it * Byte.SIZE_BITS)).toByte()
|
(this shr (it * Byte.SIZE_BITS)).toByte()
|
||||||
}.toByteArray()
|
}.toByteArray()
|
||||||
|
|
||||||
|
rotationsDao.deleteBySerial(serial)
|
||||||
|
|
||||||
return if(app.checkPermission()) {
|
return if(app.checkPermission()) {
|
||||||
|
|
||||||
val connection =
|
val connection =
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
@ -6,11 +6,11 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import llc.arma.ble.data.extensions.checkPermission
|
import llc.arma.ble.data.repository.extensions.checkPermission
|
||||||
import llc.arma.ble.data.extensions.fromByte
|
import llc.arma.ble.data.repository.extensions.fromByte
|
||||||
import llc.arma.ble.data.extensions.get2byteShortAt
|
import llc.arma.ble.data.repository.extensions.get2byteShortAt
|
||||||
import llc.arma.ble.data.extensions.get2byteUIntAt
|
import llc.arma.ble.data.repository.extensions.get2byteUIntAt
|
||||||
import llc.arma.ble.data.extensions.get4byteUIntAt
|
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.common.ProgressState
|
import llc.arma.ble.domain.common.ProgressState
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import llc.arma.ble.data.extensions.checkPermission
|
import llc.arma.ble.data.repository.extensions.checkPermission
|
||||||
import llc.arma.ble.data.extensions.get2byteShortAt
|
import llc.arma.ble.data.repository.extensions.get2byteShortAt
|
||||||
import llc.arma.ble.data.extensions.sendData
|
import llc.arma.ble.data.repository.extensions.sendData
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
|
|
@ -1,21 +1,16 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.bluetooth.BluetoothGatt
|
|
||||||
import android.bluetooth.BluetoothGattCallback
|
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
|
||||||
import android.bluetooth.BluetoothGattDescriptor
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import llc.arma.ble.data.extensions.checkPermission
|
import llc.arma.ble.data.repository.extensions.checkPermission
|
||||||
import llc.arma.ble.data.extensions.get2byteShortAt
|
import llc.arma.ble.data.repository.extensions.get2byteShortAt
|
||||||
import llc.arma.ble.data.extensions.get2byteUIntAt
|
import llc.arma.ble.data.repository.extensions.get2byteUIntAt
|
||||||
import llc.arma.ble.data.extensions.get4byteUIntAt
|
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
|
||||||
import llc.arma.ble.data.extensions.sendData
|
import llc.arma.ble.data.repository.extensions.sendData
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.common.ProgressState
|
import llc.arma.ble.domain.common.ProgressState
|
||||||
|
|
@ -27,8 +22,6 @@ import llc.arma.ble.domain.usecase.FftFrequency
|
||||||
import llc.arma.ble.domain.usecase.FftViewMode
|
import llc.arma.ble.domain.usecase.FftViewMode
|
||||||
import no.nordicsemi.android.common.core.DataByteArray
|
import no.nordicsemi.android.common.core.DataByteArray
|
||||||
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
|
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.bluetooth.BluetoothGatt
|
|
||||||
import android.bluetooth.BluetoothGattCallback
|
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import llc.arma.ble.data.extensions.checkPermission
|
import llc.arma.ble.data.repository.extensions.checkPermission
|
||||||
import llc.arma.ble.data.extensions.get2byteUIntAt
|
import llc.arma.ble.data.repository.extensions.get2byteUIntAt
|
||||||
import llc.arma.ble.data.extensions.get4byteUIntAt
|
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
|
||||||
import llc.arma.ble.data.extensions.toTemperature
|
import llc.arma.ble.data.repository.extensions.toTemperature
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.common.ProgressState
|
import llc.arma.ble.domain.common.ProgressState
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
|
import llc.arma.ble.data.db.RotationsDao
|
||||||
|
import llc.arma.ble.data.model.RotationEntity
|
||||||
|
import llc.arma.ble.data.model.WheelEntity
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RotationsRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: RotationsDao
|
||||||
|
) : RotationsRepository {
|
||||||
|
|
||||||
|
override suspend fun saveRotations(rotations: List<Rotation>) {
|
||||||
|
dao.deleteBySerialAndDateMoreThan(rotations.first().bleId, rotations.minOf { it.date } - 8)
|
||||||
|
dao.save(
|
||||||
|
rotations.map {
|
||||||
|
RotationEntity(
|
||||||
|
bleId = it.bleId,
|
||||||
|
rotations = it.rotations,
|
||||||
|
date = it.date
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRotationsBySerial(serial: String): List<Rotation> {
|
||||||
|
return dao.getBySerialSortedByDate(serial).map {
|
||||||
|
Rotation(
|
||||||
|
bleId = it.bleId,
|
||||||
|
rotations = it.rotations,
|
||||||
|
date = it.date
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteRotationsBySerial(serial: String) {
|
||||||
|
dao.deleteBySerial(serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getWheelRadiusBySerial(serial: String): Long? {
|
||||||
|
return dao.getWheelBySerial(serial)?.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setWheelRadius(serial: String, radius: Long) {
|
||||||
|
dao.save(WheelEntity(serial, radius))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
package llc.arma.ble.data
|
package llc.arma.ble.data.repository
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.icu.text.SimpleDateFormat
|
import android.icu.text.SimpleDateFormat
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
|
||||||
import llc.arma.ble.R
|
import llc.arma.ble.R
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
import llc.arma.ble.domain.repository.XlsxRepository
|
import llc.arma.ble.domain.repository.XlsxRepository
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data.extensions
|
package llc.arma.ble.data.repository.extensions
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data.extensions
|
package llc.arma.ble.data.repository.extensions
|
||||||
|
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
import llc.arma.ble.domain.usecase.AccelScale
|
import llc.arma.ble.domain.usecase.AccelScale
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data.extensions
|
package llc.arma.ble.data.repository.extensions
|
||||||
|
|
||||||
import llc.arma.ble.domain.model.BleInfo
|
import llc.arma.ble.domain.model.BleInfo
|
||||||
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult
|
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package llc.arma.ble.data.extensions
|
package llc.arma.ble.data.repository.extensions
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package llc.arma.ble.domain.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
|
||||||
|
data class Rotation(
|
||||||
|
|
||||||
|
val bleId: String,
|
||||||
|
|
||||||
|
val date: Long,
|
||||||
|
|
||||||
|
val rotations: Long
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package llc.arma.ble.domain.repository
|
||||||
|
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
|
|
||||||
|
interface RotationsRepository {
|
||||||
|
|
||||||
|
suspend fun saveRotations(rotations: List<Rotation>)
|
||||||
|
|
||||||
|
suspend fun getRotationsBySerial(serial: String): List<Rotation>
|
||||||
|
|
||||||
|
suspend fun deleteRotationsBySerial(serial: String)
|
||||||
|
|
||||||
|
suspend fun getWheelRadiusBySerial(serial: String): Long?
|
||||||
|
|
||||||
|
suspend fun setWheelRadius(serial: String, radius: Long)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package llc.arma.ble.domain.usecase
|
||||||
|
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeleteRotationsBySerial @Inject constructor (
|
||||||
|
private val rotationsRepository: RotationsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(serial: String) {
|
||||||
|
|
||||||
|
rotationsRepository.deleteRotationsBySerial(serial)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,49 @@
|
||||||
package llc.arma.ble.domain.usecase
|
package llc.arma.ble.domain.usecase
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.common.ProgressState
|
import llc.arma.ble.domain.common.ProgressState
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
import llc.arma.ble.domain.repository.BleRepository
|
import llc.arma.ble.domain.repository.BleRepository
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GetAccelerometerHistoryBySerial @Inject constructor(
|
class GetAccelerometerHistoryBySerial @Inject constructor(
|
||||||
private val bleRepository: BleRepository
|
private val bleRepository: BleRepository,
|
||||||
|
private val rotationsRepository: RotationsRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {
|
suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {
|
||||||
|
|
||||||
return bleRepository.getAccelerometerHistoryBySerial(serial)
|
return bleRepository.getAccelerometerHistoryBySerial(serial).onEach {
|
||||||
|
it.onSuccess { progress ->
|
||||||
|
if(progress is ProgressState.Finished){
|
||||||
|
val rotations = progress.data.mapNotNull { point ->
|
||||||
|
if(point is Ble.Accelerometer.HistoryPoint.Rotation){
|
||||||
|
Rotation(
|
||||||
|
bleId = serial,
|
||||||
|
date = TimeUnit.SECONDS.toMillis(TimeUnit.MILLISECONDS.toSeconds(point.date)),
|
||||||
|
rotations = point.value
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if(rotations.isNotEmpty()){
|
||||||
|
|
||||||
|
rotationsRepository.saveRotations(rotations)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package llc.arma.ble.domain.usecase
|
||||||
|
|
||||||
|
import llc.arma.ble.domain.model.Rotation
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetRotationsBySerial @Inject constructor (
|
||||||
|
private val rotationsRepository: RotationsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(serial: String): List<Rotation> {
|
||||||
|
|
||||||
|
return rotationsRepository.getRotationsBySerial(serial)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package llc.arma.ble.domain.usecase
|
||||||
|
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetWheelRadiusBySerial @Inject constructor(
|
||||||
|
private val rotationsRepository: RotationsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(serial: String): Long? {
|
||||||
|
return rotationsRepository.getWheelRadiusBySerial(serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package llc.arma.ble.domain.usecase
|
||||||
|
|
||||||
|
import llc.arma.ble.domain.repository.RotationsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SetWheelRadius @Inject constructor(
|
||||||
|
private val rotationsRepository: RotationsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(serial: String, radius: Long){
|
||||||
|
rotationsRepository.setWheelRadius(serial, radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue