add ble sort

This commit is contained in:
Vineyro 2024-06-11 10:19:15 +07:00
parent 7652ccc135
commit b915659217
6 changed files with 306 additions and 17 deletions

View File

@ -7,14 +7,14 @@ plugins {
android { android {
namespace 'llc.arma.ble' namespace 'llc.arma.ble'
compileSdk 33 compileSdk 34
defaultConfig { defaultConfig {
applicationId "llc.arma.ble" applicationId "llc.arma.ble"
minSdk 26 minSdk 26
targetSdk 33 targetSdk 34
versionCode 15 versionCode 17
versionName "1.2.15" versionName "1.2.17"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -23,6 +23,10 @@ android {
} }
buildTypes { buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
@ -59,6 +63,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.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.activity:activity-compose:1.7.2'
implementation "androidx.compose.ui:ui:1.5.0-beta01" implementation "androidx.compose.ui:ui:1.5.0-beta01"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-beta01" implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-beta01"

View File

@ -1,14 +1,15 @@
package llc.arma.ble.app.ui package llc.arma.ble.app.ui
import android.Manifest 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.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
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
@ -24,10 +25,16 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat 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.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint 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.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() {
@SuppressLint("MissingPermission")
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
installSplashScreen() 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) { 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 { } 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)
}
}
} }

View File

@ -41,6 +41,14 @@ class BleListContract {
val type: BleInfo.Type? val type: BleInfo.Type?
) : Event() ) : Event()
data class OnSortFieldChanged(
val field: State.Filter.Field
) : Event()
data class OnSortOrderChanged(
val order: State.Filter.Order
) : Event()
} }
data class State( data class State(
@ -50,12 +58,24 @@ class BleListContract {
) : ViewState { ) : ViewState {
data class Filter( data class Filter(
val sortField: Field = Field.Name,
val sortOrder: Order = Order.Asc,
val name: String = "", val name: String = "",
val mac: String = "", val mac: String = "",
val battery: ClosedFloatingPointRange<Float> = (0f)..(100f), val battery: ClosedFloatingPointRange<Float> = (0f)..(100f),
val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-10f), val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-10f),
val bleType: BleInfo.Type? = null val bleType: BleInfo.Type? = null
) ){
enum class Field {
Name, Mac, Distance, Dbm, Battery
}
enum class Order {
Asc, Desc
}
}
} }

View File

@ -128,6 +128,21 @@ fun BleListScreen(
it.serial.contains(state.filter.mac) && it.serial.contains(state.filter.mac) &&
state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) && state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
state.filter.battery.contains(it.batteryLevel.toFloat()) 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()){ if(filteredData.isEmpty()){
@ -165,7 +180,7 @@ fun BleListScreen(
items(items = filteredData.sortedBy { it.name }.reversed()) { items(items = filteredData) {
BleItem( BleItem(
ble = it, ble = it,

View File

@ -46,7 +46,7 @@ class BleListViewModel @Inject constructor(
}.launchIn(viewModelScope) }.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.OnShowFilter -> reduce(viewState.value, event)
is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event) is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnBatteryRangeChanged -> 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( private fun reduce(
state: BleListContract.State, state: BleListContract.State,
event: BleListContract.Event.OnNameFilterChanged event: BleListContract.Event.OnNameFilterChanged

View File

@ -9,7 +9,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BatteryFull import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.Bluetooth 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.Search
import androidx.compose.material.icons.rounded.ShortText import androidx.compose.material.icons.rounded.ShortText
import androidx.compose.material.icons.rounded.SignalCellularAlt 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.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
@ -39,6 +43,25 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import llc.arma.ble.domain.model.BleInfo 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 private val BleInfo.Type?.localized: String
get() { get() {
return when(this){ return when(this){
@ -69,11 +92,151 @@ fun Filter(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Column( Column(
modifier = Modifier.padding(horizontal = 12.dp) modifier = Modifier
.padding(horizontal = 12.dp)
.verticalScroll(rememberScrollState())
) { ) {
Spacer(modifier = Modifier.height(8.dp)) 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( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -87,7 +250,9 @@ fun Filter(
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
modifier = Modifier.fillMaxWidth().padding(end = 8.dp), modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded, expanded = expanded,
onExpandedChange = { onExpandedChange = {
expanded = it expanded = it
@ -95,7 +260,9 @@ fun Filter(
) { ) {
OutlinedTextField( OutlinedTextField(
modifier = Modifier.menuAnchor().fillMaxWidth(), modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
readOnly = true, readOnly = true,
value = filter.bleType.localized, value = filter.bleType.localized,
onValueChange = { }, onValueChange = { },
@ -108,7 +275,9 @@ fun Filter(
) )
ExposedDropdownMenu( ExposedDropdownMenu(
modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxWidth(), modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
expanded = expanded, expanded = expanded,
onDismissRequest = { onDismissRequest = {
expanded = false expanded = false
@ -137,6 +306,8 @@ fun Filter(
} }
Spacer(modifier = Modifier.height(8.dp))
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
){ ){