From b915659217b56500872e01f897f7a7738770d37f Mon Sep 17 00:00:00 2001 From: Vineyro Date: Tue, 11 Jun 2024 10:19:15 +0700 Subject: [PATCH] add ble sort --- app/build.gradle | 13 +- .../java/llc/arma/ble/app/ui/MainActivity.kt | 66 ++++++- .../ble/app/ui/screen/ble/BleListContract.kt | 22 ++- .../ble/app/ui/screen/ble/BleListScreen.kt | 17 +- .../ble/app/ui/screen/ble/BleListViewModel.kt | 26 ++- .../llc/arma/ble/app/ui/screen/ble/Filter.kt | 179 +++++++++++++++++- 6 files changed, 306 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d578730..81f1633 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,14 +7,14 @@ plugins { android { namespace 'llc.arma.ble' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "llc.arma.ble" minSdk 26 - targetSdk 33 - versionCode 15 - versionName "1.2.15" + targetSdk 34 + versionCode 17 + versionName "1.2.17" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -23,6 +23,10 @@ android { } buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -59,6 +63,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha01' implementation 'androidx.activity:activity-compose:1.7.2' implementation "androidx.compose.ui:ui:1.5.0-beta01" implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-beta01" diff --git a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt index 1072fbd..29ce8a1 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt @@ -1,14 +1,15 @@ package llc.arma.ble.app.ui import android.Manifest -import android.content.BroadcastReceiver +import android.annotation.SuppressLint +import android.app.Activity +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent -import android.content.IntentFilter +import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.util.Log -import android.view.KeyEvent import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent @@ -24,10 +25,16 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat +import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityCompat.startActivityForResult import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import dagger.hilt.android.AndroidEntryPoint @@ -37,13 +44,17 @@ import llc.arma.ble.app.ui.common.LocalBottomDialogState import llc.arma.ble.app.ui.screen.main.MainScreen import llc.arma.ble.app.ui.theme.BleTheme + @AndroidEntryPoint class MainActivity : ComponentActivity() { + @SuppressLint("MissingPermission") @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter + WindowCompat.setDecorFitsSystemWindows(window, false) installSplashScreen() @@ -168,9 +179,32 @@ class MainActivity : ComponentActivity() { } } + var bleEnabled by remember { + mutableStateOf(mBluetoothAdapter.isEnabled) + } + + val lifecycleOwner = LocalLifecycleOwner.current + val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState() + + LaunchedEffect(lifecycleState){ + bleEnabled = mBluetoothAdapter.isEnabled + } + if (multiplePermissionsState.allPermissionsGranted) { - MainScreen() + if(bleEnabled) { + + MainScreen() + + } else { + + val context = LocalContext.current + + LaunchedEffect(mBluetoothAdapter.isEnabled){ + val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + (context as Activity).startActivityForResult(intent, 1) + } + } } else { @@ -183,7 +217,9 @@ class MainActivity : ComponentActivity() { } } + ) + } } @@ -192,4 +228,22 @@ class MainActivity : ComponentActivity() { } +} + +@Composable +fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) { + val eventHandler = rememberUpdatedState(onEvent) + val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) + + DisposableEffect(lifecycleOwner.value) { + val lifecycle = lifecycleOwner.value.lifecycle + val observer = LifecycleEventObserver { owner, event -> + eventHandler.value(owner, event) + } + + lifecycle.addObserver(observer) + onDispose { + lifecycle.removeObserver(observer) + } + } } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt index 158d1d7..1119707 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt @@ -41,6 +41,14 @@ class BleListContract { val type: BleInfo.Type? ) : Event() + data class OnSortFieldChanged( + val field: State.Filter.Field + ) : Event() + + data class OnSortOrderChanged( + val order: State.Filter.Order + ) : Event() + } data class State( @@ -50,12 +58,24 @@ class BleListContract { ) : ViewState { data class Filter( + val sortField: Field = Field.Name, + val sortOrder: Order = Order.Asc, val name: String = "", val mac: String = "", val battery: ClosedFloatingPointRange = (0f)..(100f), val rssi: ClosedFloatingPointRange = (-100f)..(-10f), val bleType: BleInfo.Type? = null - ) + ){ + + enum class Field { + Name, Mac, Distance, Dbm, Battery + } + + enum class Order { + Asc, Desc + } + + } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt index f40132f..75863aa 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt @@ -128,6 +128,21 @@ fun BleListScreen( it.serial.contains(state.filter.mac) && state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) && state.filter.battery.contains(it.batteryLevel.toFloat()) + }.let { + when(state.filter.sortField){ + BleListContract.State.Filter.Field.Name -> it.sortedBy { it.name } + BleListContract.State.Filter.Field.Mac -> it.sortedBy { it.serial } + BleListContract.State.Filter.Field.Distance -> it.sortedBy { 10.0.pow((it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20).toFloat() } + BleListContract.State.Filter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 } + BleListContract.State.Filter.Field.Battery -> it.sortedBy { it.batteryLevel } + } + }.let { + + when(state.filter.sortOrder){ + BleListContract.State.Filter.Order.Asc -> it + BleListContract.State.Filter.Order.Desc -> it.reversed() + } + } if(filteredData.isEmpty()){ @@ -165,7 +180,7 @@ fun BleListScreen( - items(items = filteredData.sortedBy { it.name }.reversed()) { + items(items = filteredData) { BleItem( ble = it, diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt index cbd967f..59bbc6d 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt @@ -46,7 +46,7 @@ class BleListViewModel @Inject constructor( }.launchIn(viewModelScope) - delay(6_000) + delay(30_000) } @@ -68,6 +68,8 @@ class BleListViewModel @Inject constructor( is BleListContract.Event.OnShowFilter -> reduce(viewState.value, event) is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event) is BleListContract.Event.OnBatteryRangeChanged -> reduce(viewState.value, event) + is BleListContract.Event.OnSortFieldChanged -> reduce(viewState.value, event) + is BleListContract.Event.OnSortOrderChanged -> reduce(viewState.value, event) } } @@ -100,6 +102,28 @@ class BleListViewModel @Inject constructor( } } + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnSortOrderChanged + ) { + setState { + copy( + filter = this.filter.copy(sortOrder = event.order) + ) + } + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnSortFieldChanged + ) { + setState { + copy( + filter = this.filter.copy(sortField = event.field) + ) + } + } + private fun reduce( state: BleListContract.State, event: BleListContract.Event.OnNameFilterChanged diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt index df63268..1090fcc 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt @@ -9,7 +9,9 @@ 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.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.BatteryFull import androidx.compose.material.icons.rounded.Bluetooth @@ -17,6 +19,8 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.ShortText import androidx.compose.material.icons.rounded.SignalCellularAlt +import androidx.compose.material.icons.rounded.Sort +import androidx.compose.material.icons.rounded.SortByAlpha import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -39,6 +43,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import llc.arma.ble.domain.model.BleInfo +private val BleListContract.State.Filter.Order.localized: String + get() { + return when(this){ + BleListContract.State.Filter.Order.Asc -> "Прямой ↓" + BleListContract.State.Filter.Order.Desc -> "Обратный ↑" + } + } + +private val BleListContract.State.Filter.Field.localized: String + get() { + return when(this){ + BleListContract.State.Filter.Field.Name -> "Имя" + BleListContract.State.Filter.Field.Mac -> "MAC" + BleListContract.State.Filter.Field.Distance -> "Расстояние" + BleListContract.State.Filter.Field.Dbm -> "dBm" + BleListContract.State.Filter.Field.Battery -> "Заряд" + } + } + private val BleInfo.Type?.localized: String get() { return when(this){ @@ -69,11 +92,151 @@ fun Filter( Spacer(modifier = Modifier.height(16.dp)) Column( - modifier = Modifier.padding(horizontal = 12.dp) + modifier = Modifier + .padding(horizontal = 12.dp) + .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + + Icon( + imageVector = Icons.Rounded.Sort, + contentDescription = null, + modifier = Modifier.padding(12.dp) + ) + + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + modifier = Modifier + .fillMaxWidth() + .padding(end = 8.dp), + expanded = expanded, + onExpandedChange = { + expanded = it + } + ) { + + OutlinedTextField( + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), + readOnly = true, + value = filter.sortField.localized, + onValueChange = { }, + label = { Text("Сортировка") }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + } + ) + + ExposedDropdownMenu( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth(), + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + + BleListContract.State.Filter.Field.values().forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onEvent( + BleListContract.Event.OnSortFieldChanged( + selectionOption + ) + ) + expanded = false + }, + text = { + Text(text = selectionOption.localized) + } + ) + } + } + } + + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + + Icon( + imageVector = Icons.Rounded.SortByAlpha, + contentDescription = null, + modifier = Modifier.padding(12.dp) + ) + + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + modifier = Modifier + .fillMaxWidth() + .padding(end = 8.dp), + expanded = expanded, + onExpandedChange = { + expanded = it + } + ) { + + OutlinedTextField( + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), + readOnly = true, + value = filter.sortOrder.localized, + onValueChange = { }, + label = { Text("Напрвление сортировки") }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + } + ) + + ExposedDropdownMenu( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth(), + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + + BleListContract.State.Filter.Order.values().forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onEvent( + BleListContract.Event.OnSortOrderChanged( + selectionOption + ) + ) + expanded = false + }, + text = { + Text(text = selectionOption.localized) + } + ) + } + } + } + + } + + Spacer(modifier = Modifier.height(8.dp)) + Row( verticalAlignment = Alignment.CenterVertically ) { @@ -87,7 +250,9 @@ fun Filter( var expanded by remember { mutableStateOf(false) } ExposedDropdownMenuBox( - modifier = Modifier.fillMaxWidth().padding(end = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(end = 8.dp), expanded = expanded, onExpandedChange = { expanded = it @@ -95,7 +260,9 @@ fun Filter( ) { OutlinedTextField( - modifier = Modifier.menuAnchor().fillMaxWidth(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), readOnly = true, value = filter.bleType.localized, onValueChange = { }, @@ -108,7 +275,9 @@ fun Filter( ) ExposedDropdownMenu( - modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxWidth(), + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth(), expanded = expanded, onDismissRequest = { expanded = false @@ -137,6 +306,8 @@ fun Filter( } + Spacer(modifier = Modifier.height(8.dp)) + Row( verticalAlignment = Alignment.CenterVertically ){