mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-03 21:53:55 -04:00
Merge remote-tracking branch 'origin/release' into release
This commit is contained in:
@@ -3,6 +3,9 @@ package com.geeksville.mesh
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -15,4 +18,14 @@ object ApplicationModule {
|
||||
fun provideSharedPreferences(application: Application): SharedPreferences {
|
||||
return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideProcessLifecycleOwner(): LifecycleOwner {
|
||||
return ProcessLifecycleOwner.get()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideProcessLifecycle(processLifecycleOwner: LifecycleOwner): Lifecycle {
|
||||
return processLifecycleOwner.lifecycle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.geeksville.mesh
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Wrapper around `Dispatchers` to allow for easier testing when using dispatchers
|
||||
* in injected classes.
|
||||
*/
|
||||
class CoroutineDispatchers @Inject constructor() {
|
||||
val main = Dispatchers.Main
|
||||
val mainImmediate = Dispatchers.Main.immediate
|
||||
val default = Dispatchers.Default
|
||||
val io = Dispatchers.IO
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.*
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
@@ -40,6 +39,7 @@ import com.geeksville.android.ServiceClient
|
||||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.ChannelSet
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
@@ -134,11 +134,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
// Used to schedule a coroutine in the GUI thread
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
bluetoothManager.adapter
|
||||
}
|
||||
|
||||
val bluetoothViewModel: BluetoothViewModel by viewModels()
|
||||
val model: UIViewModel by viewModels()
|
||||
|
||||
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
|
||||
@@ -187,28 +183,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
}
|
||||
}
|
||||
|
||||
private val btStateReceiver = BluetoothStateReceiver {
|
||||
updateBluetoothEnabled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't tell our app we have bluetooth until we have bluetooth _and_ location access
|
||||
*/
|
||||
private fun updateBluetoothEnabled() {
|
||||
var enabled = false // assume failure
|
||||
|
||||
if (hasConnectPermission()) {
|
||||
/// ask the adapter if we have access
|
||||
bluetoothAdapter?.apply {
|
||||
enabled = isEnabled
|
||||
}
|
||||
} else
|
||||
errormsg("Still missing needed bluetooth permissions")
|
||||
|
||||
debug("Detected our bluetooth access=$enabled")
|
||||
model.bluetoothEnabled.value = enabled
|
||||
}
|
||||
|
||||
/** Get the minimum permissions our app needs to run correctly
|
||||
*/
|
||||
private fun getMinimumPermissions(): List<String> {
|
||||
@@ -381,7 +355,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
}
|
||||
}
|
||||
|
||||
updateBluetoothEnabled()
|
||||
bluetoothViewModel.refreshState()
|
||||
}
|
||||
|
||||
|
||||
@@ -445,12 +419,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
/// Set theme
|
||||
setUITheme(prefs)
|
||||
|
||||
/// Set initial bluetooth state
|
||||
updateBluetoothEnabled()
|
||||
|
||||
/// We now want to be informed of bluetooth state
|
||||
registerReceiver(btStateReceiver, btStateReceiver.intentFilter)
|
||||
|
||||
/* not yet working
|
||||
// Configure sign-in to request the user's ID, email address, and basic
|
||||
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
|
||||
@@ -569,7 +537,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(btStateReceiver)
|
||||
unregisterMeshReceiver()
|
||||
mainScope.cancel("Activity going away")
|
||||
super.onDestroy()
|
||||
@@ -1003,17 +970,17 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// Ask to start bluetooth if no USB devices are visible
|
||||
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
|
||||
if (!isInTestLab && !hasUSB) {
|
||||
if (hasConnectPermission()) {
|
||||
bluetoothAdapter?.let {
|
||||
if (!it.isEnabled) {
|
||||
bluetoothViewModel.enabled.observe(this) { enabled ->
|
||||
if (!enabled) {
|
||||
// Ask to start bluetooth if no USB devices are visible
|
||||
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
|
||||
if (!isInTestLab && !hasUSB) {
|
||||
if (hasConnectPermission()) {
|
||||
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
|
||||
}
|
||||
} else requestPermission()
|
||||
}
|
||||
} else requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -16,8 +16,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
|
||||
packetDao.getAllPacket(MAX_ITEMS)
|
||||
}
|
||||
|
||||
suspend fun getAllPacketsInReceiveOrder(): Flow<List<Packet>> = withContext(Dispatchers.IO) {
|
||||
packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
|
||||
suspend fun getAllPacketsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<Packet>> = withContext(Dispatchers.IO) {
|
||||
packetDao.getAllPacketsInReceiveOrder(maxItems)
|
||||
}
|
||||
|
||||
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -16,7 +16,7 @@ data class Packet(@PrimaryKey val uuid: String,
|
||||
@ColumnInfo(name = "message") val raw_message: String
|
||||
) {
|
||||
|
||||
val proto: MeshProtos.MeshPacket?
|
||||
val meshPacket: MeshProtos.MeshPacket?
|
||||
get() {
|
||||
if (message_type == "packet") {
|
||||
val builder = MeshProtos.MeshPacket.newBuilder()
|
||||
@@ -28,13 +28,27 @@ data class Packet(@PrimaryKey val uuid: String,
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val nodeInfo: MeshProtos.NodeInfo?
|
||||
get() {
|
||||
if (message_type == "NodeInfo") {
|
||||
val builder = MeshProtos.NodeInfo.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val position: MeshProtos.Position?
|
||||
get() {
|
||||
return proto?.run {
|
||||
return meshPacket?.run {
|
||||
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
|
||||
return MeshProtos.Position.parseFrom(decoded.payload)
|
||||
}
|
||||
return null
|
||||
}
|
||||
} ?: nodeInfo?.position
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.geeksville.mesh.model
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Thin view model which adapts the view layer to the `BluetoothRepository`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class BluetoothViewModel @Inject constructor(
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
) : ViewModel() {
|
||||
fun refreshState() = bluetoothRepository.refreshState()
|
||||
|
||||
val enabled = bluetoothRepository.enabled.asLiveData()
|
||||
}
|
||||
@@ -74,10 +74,6 @@ class UIViewModel @Inject constructor(
|
||||
debug("ViewModel created")
|
||||
}
|
||||
|
||||
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.insert(packet)
|
||||
}
|
||||
|
||||
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.deleteAll()
|
||||
}
|
||||
@@ -229,10 +225,6 @@ class UIViewModel @Inject constructor(
|
||||
val ownerName = object : MutableLiveData<String>("MrIDE Test") {
|
||||
}
|
||||
|
||||
|
||||
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
|
||||
}
|
||||
|
||||
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) {
|
||||
override fun setValue(value: Boolean) {
|
||||
super.setValue(value)
|
||||
@@ -243,9 +235,6 @@ class UIViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/// If the app was launched because we received a new channel intent, the Url will be here
|
||||
var requestedChannelUrl: Uri? = null
|
||||
|
||||
// clean up all this nasty owner state management FIXME
|
||||
fun setOwner(s: String? = null) {
|
||||
|
||||
@@ -283,66 +272,83 @@ class UIViewModel @Inject constructor(
|
||||
// Capture the current node value while we're still on main thread
|
||||
val nodes = nodeDB.nodes.value ?: emptyMap()
|
||||
|
||||
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
|
||||
meshPosition?.let { Position(it) }.takeIf {
|
||||
it?.isValid() == true
|
||||
}
|
||||
}
|
||||
|
||||
writeToUri(file_uri) { writer ->
|
||||
// Create a map of nodes keyed by their ID
|
||||
val nodesById = nodes.values.associateBy { it.num }
|
||||
val nodesById = nodes.values.associateBy { it.num }.toMutableMap()
|
||||
val nodePositions = mutableMapOf<Int, MeshProtos.Position?>()
|
||||
|
||||
writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
|
||||
|
||||
// Packets are ordered by time, we keep most recent position of
|
||||
// our device in localNodePosition.
|
||||
var localNodePosition: MeshProtos.Position? = null
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
|
||||
repository.getAllPacketsInReceiveOrder().first().forEach { packet ->
|
||||
packet.proto?.let { proto ->
|
||||
repository.getAllPacketsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
|
||||
// If we get a NodeInfo packet, use it to update our position data (if valid)
|
||||
packet.nodeInfo?.let { nodeInfo ->
|
||||
positionToPos.invoke(nodeInfo.position)?.let { _ ->
|
||||
nodePositions[nodeInfo.num] = nodeInfo.position
|
||||
}
|
||||
}
|
||||
|
||||
packet.meshPacket?.let { proto ->
|
||||
// If the packet contains position data then use it to update, if valid
|
||||
packet.position?.let { position ->
|
||||
if (proto.from == myNodeNum) {
|
||||
localNodePosition = position
|
||||
} else {
|
||||
val rxDateTime = dateFormat.format(packet.received_date)
|
||||
val rxFrom = proto.from.toUInt()
|
||||
val senderName = nodesById[proto.from]?.user?.longName ?: ""
|
||||
|
||||
// sender lat & long
|
||||
val senderPos = packet.position
|
||||
?.let { p -> Position(p) }
|
||||
?.takeIf { p -> p.isValid() }
|
||||
val senderLat = senderPos?.latitude ?: ""
|
||||
val senderLong = senderPos?.longitude ?: ""
|
||||
|
||||
// rx lat, long, and elevation
|
||||
val rxPos = localNodePosition
|
||||
?.let { p -> Position(p) }
|
||||
?.takeIf { p -> p.isValid() }
|
||||
val rxLat = rxPos?.latitude ?: ""
|
||||
val rxLong = rxPos?.longitude ?: ""
|
||||
val rxAlt = rxPos?.altitude ?: ""
|
||||
val rxSnr = "%f".format(proto.rxSnr)
|
||||
|
||||
// Calculate the distance if both positions are valid
|
||||
val dist = if (senderPos == null || rxPos == null) {
|
||||
""
|
||||
} else {
|
||||
positionToMeter(
|
||||
localNodePosition!!,
|
||||
position
|
||||
).roundToInt().toString()
|
||||
}
|
||||
|
||||
val hopLimit = proto.hopLimit
|
||||
|
||||
val payload = when {
|
||||
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
|
||||
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
|
||||
.replace("\"", "\\\"") + "\""
|
||||
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
|
||||
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
|
||||
positionToPos.invoke(position)?.let { _ ->
|
||||
nodePositions[proto.from] = position
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out of our results any packet that doesn't report SNR. This
|
||||
// is primarily ADMIN_APP.
|
||||
if (proto.rxSnr > 0.0f) {
|
||||
val rxDateTime = dateFormat.format(packet.received_date)
|
||||
val rxFrom = proto.from.toUInt()
|
||||
val senderName = nodesById[proto.from]?.user?.longName ?: ""
|
||||
|
||||
// sender lat & long
|
||||
val senderPosition = nodePositions[proto.from]
|
||||
val senderPos = positionToPos.invoke(senderPosition)
|
||||
val senderLat = senderPos?.latitude ?: ""
|
||||
val senderLong = senderPos?.longitude ?: ""
|
||||
|
||||
// rx lat, long, and elevation
|
||||
val rxPosition = nodePositions[myNodeNum]
|
||||
val rxPos = positionToPos.invoke(rxPosition)
|
||||
val rxLat = rxPos?.latitude ?: ""
|
||||
val rxLong = rxPos?.longitude ?: ""
|
||||
val rxAlt = rxPos?.altitude ?: ""
|
||||
val rxSnr = "%f".format(proto.rxSnr)
|
||||
|
||||
// Calculate the distance if both positions are valid
|
||||
|
||||
val dist = if (senderPos == null || rxPos == null) {
|
||||
""
|
||||
} else {
|
||||
positionToMeter(
|
||||
rxPosition!!, // Use rxPosition but only if rxPos was valid
|
||||
senderPosition!! // Use senderPosition but only if senderPos was valid
|
||||
).roundToInt().toString()
|
||||
}
|
||||
|
||||
val hopLimit = proto.hopLimit
|
||||
|
||||
val payload = when {
|
||||
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
|
||||
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
|
||||
.replace("\"", "\\\"") + "\""
|
||||
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
|
||||
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.geeksville.mesh.repository.bluetooth
|
||||
|
||||
import android.app.Application
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.android.hasConnectPermission
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Repository responsible for maintaining and updating the state of Bluetooth availability.
|
||||
*/
|
||||
@Singleton
|
||||
class BluetoothRepository @Inject constructor(
|
||||
private val application: Application,
|
||||
private val bluetoothAdapterLazy: dagger.Lazy<BluetoothAdapter>,
|
||||
private val bluetoothStateReceiverLazy: dagger.Lazy<BluetoothStateReceiver>,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val processLifecycle: Lifecycle,
|
||||
) : Logging {
|
||||
internal val enabledInternal = MutableStateFlow(false)
|
||||
val enabled: StateFlow<Boolean> = enabledInternal
|
||||
|
||||
init {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
updateBluetoothEnabled()
|
||||
bluetoothStateReceiverLazy.get().let { receiver ->
|
||||
application.registerReceiver(receiver, receiver.intentFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshState() {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
updateBluetoothEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateBluetoothEnabled() {
|
||||
if (application.hasConnectPermission()) {
|
||||
/// ask the adapter if we have access
|
||||
bluetoothAdapterLazy.get()?.let { adapter ->
|
||||
enabledInternal.emit(adapter.isEnabled)
|
||||
}
|
||||
} else
|
||||
errormsg("Still missing needed bluetooth permissions")
|
||||
|
||||
debug("Detected our bluetooth access=${enabled.value}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.geeksville.mesh.repository.bluetooth
|
||||
|
||||
import android.app.Application
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BluetoothRepositoryModule {
|
||||
companion object {
|
||||
@Provides
|
||||
fun provideBluetoothManager(application: Application): BluetoothManager {
|
||||
return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideBluetoothAdapter(service: BluetoothManager): BluetoothAdapter {
|
||||
return service.adapter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.geeksville.mesh.repository.bluetooth
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.util.exceptionReporter
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A helper class to call onChanged when bluetooth is enabled or disabled
|
||||
*/
|
||||
class BluetoothStateReceiver @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
private val processLifecycle: Lifecycle,
|
||||
) : BroadcastReceiver() {
|
||||
internal val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
||||
when (intent.bluetoothAdapterState) {
|
||||
// Simulate a disconnection if the user disables bluetooth entirely
|
||||
BluetoothAdapter.STATE_OFF -> emitState(false)
|
||||
BluetoothAdapter.STATE_ON -> emitState(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitState(newState: Boolean) {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
bluetoothRepository.enabledInternal.emit(newState)
|
||||
}
|
||||
}
|
||||
|
||||
private val Intent.bluetoothAdapterState: Int
|
||||
get() = getIntExtra(BluetoothAdapter.EXTRA_STATE,-1)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.geeksville.util.exceptionReporter
|
||||
|
||||
/**
|
||||
* A helper class to call onChanged when bluetooth is enabled or disabled
|
||||
*/
|
||||
class BluetoothStateReceiver(
|
||||
private val onChanged: (Boolean) -> Unit
|
||||
) : BroadcastReceiver() {
|
||||
|
||||
val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
||||
when (intent.bluetoothAdapterState) {
|
||||
// Simulate a disconnection if the user disables bluetooth entirely
|
||||
BluetoothAdapter.STATE_OFF -> onChanged(false)
|
||||
BluetoothAdapter.STATE_ON -> onChanged(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Intent.bluetoothAdapterState: Int
|
||||
get() = getIntExtra(
|
||||
BluetoothAdapter.EXTRA_STATE,
|
||||
-1
|
||||
)
|
||||
}
|
||||
@@ -2,24 +2,27 @@ package com.geeksville.mesh.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.android.BinaryLogFile
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.IRadioInterfaceService
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.util.anonymize
|
||||
import com.geeksville.util.ignoreException
|
||||
import com.geeksville.util.toRemoteExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
open class RadioNotConnectedException(message: String = "Not connected to radio") :
|
||||
@@ -35,8 +38,18 @@ open class RadioNotConnectedException(message: String = "Not connected to radio"
|
||||
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
|
||||
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class RadioInterfaceService : Service(), Logging {
|
||||
|
||||
// The following is due to the fact that AIDL prevents us from extending from `LifecycleService`:
|
||||
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleDispatcher.lifecycle }
|
||||
private val lifecycleDispatcher: ServiceLifecycleDispatcher by lazy {
|
||||
ServiceLifecycleDispatcher(lifecycleOwner)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var bluetoothRepository: BluetoothRepository
|
||||
|
||||
companion object : Logging {
|
||||
/**
|
||||
* The RECEIVED_FROMRADIO
|
||||
@@ -104,7 +117,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||
@SuppressLint("NewApi")
|
||||
fun getBondedDeviceAddress(context: Context): String? {
|
||||
// If the user has unpaired our device, treat things as if we don't have one
|
||||
var address = getDeviceAddress(context)
|
||||
val address = getDeviceAddress(context)
|
||||
|
||||
/// Interfaces can filter addresses to indicate that address is no longer acceptable
|
||||
if (address != null) {
|
||||
@@ -142,16 +155,6 @@ class RadioInterfaceService : Service(), Logging {
|
||||
/// true if our interface is currently connected to a device
|
||||
private var isConnected = false
|
||||
|
||||
/**
|
||||
* If the user turns on bluetooth after we start, make sure to try and reconnected then
|
||||
*/
|
||||
private val bluetoothStateReceiver = BluetoothStateReceiver { enabled ->
|
||||
if (enabled)
|
||||
startInterface() // If bluetooth just got turned on, try to restart our ble link (which might be bluetooth)
|
||||
else if (radioIf is BluetoothInterface)
|
||||
stopInterface() // Was using bluetooth, need to shutdown
|
||||
}
|
||||
|
||||
private fun broadcastConnectionChanged(isConnected: Boolean, isPermanent: Boolean) {
|
||||
debug("Broadcasting connection=$isConnected")
|
||||
val intent = Intent(RADIO_CONNECTED_ACTION)
|
||||
@@ -197,19 +200,35 @@ class RadioInterfaceService : Service(), Logging {
|
||||
|
||||
override fun onCreate() {
|
||||
runningService = this
|
||||
lifecycleDispatcher.onServicePreSuperOnCreate()
|
||||
super.onCreate()
|
||||
registerReceiver(bluetoothStateReceiver, bluetoothStateReceiver.intentFilter)
|
||||
|
||||
lifecycleOwner.lifecycle.coroutineScope.launch {
|
||||
bluetoothRepository.enabled.collect { enabled ->
|
||||
if (enabled) {
|
||||
startInterface()
|
||||
} else {
|
||||
stopInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
lifecycleDispatcher.onServicePreSuperOnStart()
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(bluetoothStateReceiver)
|
||||
stopInterface()
|
||||
serviceScope.cancel("Destroying RadioInterface")
|
||||
runningService = null
|
||||
lifecycleDispatcher.onServicePreSuperOnDestroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
lifecycleDispatcher.onServicePreSuperOnBind()
|
||||
return binder
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.*
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
|
||||
@@ -447,6 +448,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val scanModel: BTScanModel by activityViewModels()
|
||||
private val bluetoothViewModel: BluetoothViewModel by activityViewModels()
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
// FIXME - move this into a standard GUI helper class
|
||||
@@ -624,7 +626,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = regionAdapter
|
||||
|
||||
model.bluetoothEnabled.observe(viewLifecycleOwner) {
|
||||
bluetoothViewModel.enabled.observe(viewLifecycleOwner) {
|
||||
if (it) binding.changeRadioButton.show()
|
||||
else binding.changeRadioButton.hide()
|
||||
}
|
||||
@@ -813,7 +815,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) {
|
||||
binding.warningNotPaired.visibility = View.GONE
|
||||
// binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
|
||||
} else if (model.bluetoothEnabled.value == true){
|
||||
} else if (bluetoothViewModel.enabled.value == true){
|
||||
binding.warningNotPaired.visibility = View.VISIBLE
|
||||
binding.scanStatusText.text = getString(R.string.not_paired_yet)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user