From 1294eee8e39ad69f21ebdf3253c62db1b1372062 Mon Sep 17 00:00:00 2001 From: Mike Cumings Date: Fri, 25 Feb 2022 14:14:50 -0800 Subject: [PATCH 1/7] CSV export improvements to make it more reliable (cherry picked from commit 16d2b2e5f34e2818f0e738dbed5d73f9dc2ee716) --- .../mesh/database/PacketRepository.kt | 4 +- .../geeksville/mesh/database/entity/Packet.kt | 20 ++- .../java/com/geeksville/mesh/model/UIState.kt | 115 ++++++++++-------- 3 files changed, 85 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt index 06a7ff841..e8a7077c9 100644 --- a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt @@ -16,8 +16,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz packetDao.getAllPacket(MAX_ITEMS) } - suspend fun getAllPacketsInReceiveOrder(): Flow> = withContext(Dispatchers.IO) { - packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS) + suspend fun getAllPacketsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow> = withContext(Dispatchers.IO) { + packetDao.getAllPacketsInReceiveOrder(maxItems) } suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt index ad516e1ec..bc775f205 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 6b0600e2e..e4b5dfe51 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -283,66 +283,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() 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") + } } } } From c0fe9213f180df25f794ca54a0e8bdfcd94aeac9 Mon Sep 17 00:00:00 2001 From: Mike Cumings Date: Sat, 26 Feb 2022 22:59:20 -0800 Subject: [PATCH 2/7] Issue #369 - Use repository pattern for bluetooth state (cherry picked from commit b3878a4240498ee35a95ba75a2277894c62bd9d1) --- .../com/geeksville/mesh/ApplicationModule.kt | 13 +++++ .../geeksville/mesh/CoroutineDispatchers.kt | 15 +++++ .../java/com/geeksville/mesh/MainActivity.kt | 55 ++++-------------- .../mesh/model/BluetoothViewModel.kt | 19 +++++++ .../java/com/geeksville/mesh/model/UIState.kt | 11 ---- .../bluetooth/BluetoothRepository.kt | 56 +++++++++++++++++++ .../bluetooth/BluetoothRepositoryModule.kt | 26 +++++++++ .../bluetooth/BluetoothStateReceiver.kt | 43 ++++++++++++++ .../mesh/service/BluetoothStateReceiver.kt | 34 ----------- .../mesh/service/RadioInterfaceService.kt | 55 ++++++++++++------ .../geeksville/mesh/ui/SettingsFragment.kt | 6 +- 11 files changed, 224 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/CoroutineDispatchers.kt create mode 100644 app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt delete mode 100644 app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt index 834bfea7d..b84d8b553 100644 --- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt +++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/CoroutineDispatchers.kt b/app/src/main/java/com/geeksville/mesh/CoroutineDispatchers.kt new file mode 100644 index 000000000..9922b19eb --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/CoroutineDispatchers.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 044dbf6f9..0782ec62f 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 { @@ -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 { diff --git a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt new file mode 100644 index 000000000..87631a685 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index e4b5dfe51..11e5daf5b 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -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("MrIDE Test") { } - - val bluetoothEnabled = object : MutableLiveData(false) { - } - val provideLocation = object : MutableLiveData(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) { diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt new file mode 100644 index 000000000..5f9a6d159 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt @@ -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, + private val bluetoothStateReceiverLazy: dagger.Lazy, + private val dispatchers: CoroutineDispatchers, + private val processLifecycle: Lifecycle, +) : Logging { + internal val enabledInternal = MutableStateFlow(false) + val enabled: StateFlow = 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}") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt new file mode 100644 index 000000000..9aa992bc8 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt new file mode 100644 index 000000000..2c288ef34 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt deleted file mode 100644 index a4edefb51..000000000 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index bd9b5eead..7ca4ea79f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -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 } diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index b44a31fd1..89717f296 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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) } From 49188adc369eb07187f468daed865c96ab47d9bb Mon Sep 17 00:00:00 2001 From: Mike Cumings Date: Sun, 27 Feb 2022 11:35:22 -0800 Subject: [PATCH 3/7] Issue #369 - Expand bluetooth repository use cases Changes: - Adds support for obtaining bonded devices - Adds support for obtaining BLE scanner - Consolidates state into a single, immutable data class instance - Simplified and renamed broadcast receiver - Renamed view model permissionsUpdated fun to identify the intended use (cherry picked from commit 9592fd68de5741aacbf2765365ae196e4659d9e2) --- .../java/com/geeksville/mesh/MainActivity.kt | 4 +- .../mesh/model/BluetoothViewModel.kt | 9 +- ...eiver.kt => BluetoothBroadcastReceiver.kt} | 20 +---- .../bluetooth/BluetoothRepository.kt | 82 +++++++++++++++---- .../bluetooth/BluetoothRepositoryModule.kt | 8 +- .../repository/bluetooth/BluetoothState.kt | 16 ++++ .../mesh/service/RadioInterfaceService.kt | 4 +- 7 files changed, 99 insertions(+), 44 deletions(-) rename app/src/main/java/com/geeksville/mesh/repository/bluetooth/{BluetoothStateReceiver.kt => BluetoothBroadcastReceiver.kt} (58%) create mode 100644 app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 0782ec62f..d7bf8c0dd 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -134,7 +134,7 @@ class MainActivity : AppCompatActivity(), Logging, // Used to schedule a coroutine in the GUI thread private val mainScope = CoroutineScope(Dispatchers.Main + Job()) - val bluetoothViewModel: BluetoothViewModel by viewModels() + private val bluetoothViewModel: BluetoothViewModel by viewModels() val model: UIViewModel by viewModels() data class TabInfo(val text: String, val icon: Int, val content: Fragment) @@ -355,7 +355,7 @@ class MainActivity : AppCompatActivity(), Logging, } } - bluetoothViewModel.refreshState() + bluetoothViewModel.permissionsUpdated() } diff --git a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt index 87631a685..6562f41be 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import com.geeksville.mesh.repository.bluetooth.BluetoothRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.map import javax.inject.Inject /** @@ -13,7 +14,11 @@ import javax.inject.Inject class BluetoothViewModel @Inject constructor( private val bluetoothRepository: BluetoothRepository, ) : ViewModel() { - fun refreshState() = bluetoothRepository.refreshState() + /** + * Called when permissions have been updated. This causes an explicit refresh of the + * bluetooth state. + */ + fun permissionsUpdated() = bluetoothRepository.refreshState() - val enabled = bluetoothRepository.enabled.asLiveData() + val enabled = bluetoothRepository.state.map { it.enabled }.asLiveData() } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothBroadcastReceiver.kt similarity index 58% rename from app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt rename to app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothBroadcastReceiver.kt index 2c288ef34..3f4c2ee79 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothBroadcastReceiver.kt @@ -5,20 +5,14 @@ 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, +class BluetoothBroadcastReceiver @Inject constructor( + private val bluetoothRepository: BluetoothRepository ) : BroadcastReceiver() { internal val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering @@ -26,18 +20,12 @@ class BluetoothStateReceiver @Inject constructor( 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) + BluetoothAdapter.STATE_OFF -> bluetoothRepository.refreshState() + BluetoothAdapter.STATE_ON -> bluetoothRepository.refreshState() } } } - 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) } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt index 5f9a6d159..4502783ae 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt @@ -1,15 +1,19 @@ package com.geeksville.mesh.repository.bluetooth +import android.annotation.SuppressLint import android.app.Application import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.le.BluetoothLeScanner 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.delay +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -19,18 +23,22 @@ import javax.inject.Singleton @Singleton class BluetoothRepository @Inject constructor( private val application: Application, - private val bluetoothAdapterLazy: dagger.Lazy, - private val bluetoothStateReceiverLazy: dagger.Lazy, + private val bluetoothAdapterLazy: dagger.Lazy, + private val bluetoothBroadcastReceiverLazy: dagger.Lazy, private val dispatchers: CoroutineDispatchers, private val processLifecycle: Lifecycle, ) : Logging { - internal val enabledInternal = MutableStateFlow(false) - val enabled: StateFlow = enabledInternal + private val _state = MutableStateFlow(BluetoothState( + // Assume we have permission until we get our initial state update to prevent premature + // notifications to the user. + hasPermissions = true + )) + val state: StateFlow = _state.asStateFlow() init { processLifecycle.coroutineScope.launch(dispatchers.default) { - updateBluetoothEnabled() - bluetoothStateReceiverLazy.get().let { receiver -> + updateBluetoothState() + bluetoothBroadcastReceiverLazy.get().let { receiver -> application.registerReceiver(receiver, receiver.intentFilter) } } @@ -38,19 +46,57 @@ class BluetoothRepository @Inject constructor( fun refreshState() { processLifecycle.coroutineScope.launch(dispatchers.default) { - updateBluetoothEnabled() + updateBluetoothState() } } - 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") + fun getRemoteDevice(address: String): BluetoothDevice? { + return bluetoothAdapterLazy.get()?.getRemoteDevice(address) + } - debug("Detected our bluetooth access=${enabled.value}") + fun getBluetoothLeScanner(): BluetoothLeScanner? { + return bluetoothAdapterLazy.get()?.bluetoothLeScanner + } + + @SuppressLint("MissingPermission") + internal suspend fun updateBluetoothState() { + val newState: BluetoothState = bluetoothAdapterLazy.get()?.takeIf { + application.hasConnectPermission().also { hasPerms -> + if (!hasPerms) errormsg("Still missing needed bluetooth permissions") + } + }?.let { adapter -> + /// ask the adapter if we have access + BluetoothState( + hasPermissions = true, + enabled = adapter.isEnabled, + bondedDevices = createBondedDevicesFlow(adapter), + ) + } ?: BluetoothState() + + _state.emit(newState) + debug("Detected our bluetooth access=$newState") + } + + /** + * Creates a cold Flow used to obtain the set of bonded devices. + */ + @SuppressLint("MissingPermission") // Already checked prior to calling + private suspend fun createBondedDevicesFlow(adapter: BluetoothAdapter): Flow>? { + return if (adapter.isEnabled) { + flow> { + withContext(dispatchers.default) { + while (true) { + emit(adapter.bondedDevices) + delay(REFRESH_DELAY_MS) + } + } + }.flowOn(dispatchers.default) + } else { + null + } + } + + companion object { + const val REFRESH_DELAY_MS = 1000L } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt index 9aa992bc8..7974b9b31 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt @@ -14,13 +14,13 @@ import dagger.hilt.components.SingletonComponent interface BluetoothRepositoryModule { companion object { @Provides - fun provideBluetoothManager(application: Application): BluetoothManager { - return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + fun provideBluetoothManager(application: Application): BluetoothManager? { + return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager? } @Provides - fun provideBluetoothAdapter(service: BluetoothManager): BluetoothAdapter { - return service.adapter + fun provideBluetoothAdapter(service: BluetoothManager?): BluetoothAdapter? { + return service?.adapter } } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt new file mode 100644 index 000000000..895afc9d9 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt @@ -0,0 +1,16 @@ +package com.geeksville.mesh.repository.bluetooth + +import android.bluetooth.BluetoothDevice +import kotlinx.coroutines.flow.Flow + +/** + * A snapshot in time of the state of the bluetooth subsystem. + */ +data class BluetoothState( + /** Whether we have adequate permissions to query bluetooth state */ + val hasPermissions: Boolean = false, + /** If we have adequate permissions and bluetooth is enabled */ + val enabled: Boolean = false, + /** If enabled, a cold flow of the currently bonded devices */ + val bondedDevices: Flow>? = null +) diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index 7ca4ea79f..9b6366699 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -204,8 +204,8 @@ class RadioInterfaceService : Service(), Logging { super.onCreate() lifecycleOwner.lifecycle.coroutineScope.launch { - bluetoothRepository.enabled.collect { enabled -> - if (enabled) { + bluetoothRepository.state.collect { state -> + if (state.enabled) { startInterface() } else { stopInterface() From 598ec54cf3fc370aced85856db03934872d47fd0 Mon Sep 17 00:00:00 2001 From: andrekir Date: Wed, 2 Mar 2022 14:23:41 -0300 Subject: [PATCH 4/7] anonymize sendPosition --- app/src/main/java/com/geeksville/mesh/service/MeshService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index ac0737ce8..fe3672049 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1560,7 +1560,7 @@ class MeshService : Service(), Logging { try { val mi = myNodeInfo if (mi != null) { - debug("Sending our position/time to=$destNum lat=$lat, lon=$lon, alt=$alt") + debug("Sending our position/time to=$destNum lat=${lat.anonymize}, lon=${lon.anonymize}, alt=$alt") val position = MeshProtos.Position.newBuilder().also { it.longitudeI = Position.degI(lon) From ede48be4f310b03239f73687f0a3d264335ef9a9 Mon Sep 17 00:00:00 2001 From: andrekir Date: Wed, 2 Mar 2022 14:25:35 -0300 Subject: [PATCH 5/7] fix "Multiple substitutions specified in non-positional format of string resource string/connected_count" --- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 71 +--------------------- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-ht/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko-rKR/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-sl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 19 files changed, 19 insertions(+), 88 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index d29c99100..23fb43959 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -48,7 +48,7 @@ Sdílet Odpojeno Zařízení spí - Pripojeno: %s z %s je online + Pripojeno: %1$s z %2$s je online Seznam vysílačů v síti Aktualizace softwaru Připojeno k vysílači (%s) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 72f5ff439..31072a981 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1,142 +1,73 @@ - Ρυθμίσεις - Όνομα Καναλιού - Επιλογές Καναλιού - Κοινή χρήση - Κώδικας QR - Αναίρεση - Κατάσταση Σύνδεσης - Εικονίδιο εφαρμογής - Άγνωστο Όνομα Χρήστη - Avatar Χρήστη - - Βρήκα το σημείο, είναι διπλα στη τίγρη. Φοβάμαι λίγο. - Αποστολή κειμένου - Δεν έχετε κάνει ακόμη pair μια συσκευή συμβατή με Meshtastic με το τηλέφωνο. Παρακαλώ κάντε pair μια συσκευή και ορίστε το όνομα χρήστη.\n\nΗ εφαρμογή ανοιχτού κώδικα βρίσκεται σε alpha-testing, αν εντοπίσετε προβλήματα παρακαλώ δημοσιεύστε τα στο forum: meshtastic.discourse.group.\n\nΠερισσότερες πληροφορίες στην ιστοσελίδα - www.meshtastic.org. - Όνομα Χρήστη αναιρέθηκε - Όνομα - Ανώνυμα στατιστικά χρήσης και αναφορές crash. - Αναζήτηση συσκευών Meshtastic … - Η εφαρμογή απαιτεί bluetooth πρόσβαση. Παρακαλώ παρέχεται σχετική άδεια χρήσης στις ρυθμίσεις του android. - Σφάλμα - η εφαρμογή απαιτεί bluetooth - Αρχή pairing - Pairing απέτυχε - Διεύθυνση URL για συμμετοχή σε Meshtastic mesh - Αποδοχή - Ακύρωση - Αλλαγή καναλιού - Είστε βέβαιοι ότι θέλετε να αλλάξετε κανάλι? Η επικοινωνία με άλλες συσκευές θα σταματήσεις μέχρι να μοιραστείτε τις ρυθμίσεις του νέου καναλιού. - Λήψη URL νέου καναλιού - Θέλετε να αλλάξετε ‘%s’ κανάλι? - Έχετε απενεργοποιήσει την ανάλυση δεδομένων. Δυστυχώς ο πάροχος χαρτών μας (mapbox) απαιτεί η ανάλυση δεδομένων να επιτρέπεται στο ‘δωρεάν’ πακέτο. Επομένως έχουμε απενεργοποιήσει το χάρτη.\n\n Αν επιθυμείτε να δείτε το χάρτη, θα πρέπει να ενεργοποιήσετε την ανάλυση δεδομένων στις Ρυθμίσεις (επίσης - προσωρινά - θα πρέπει να επανεκκινήσετε την εφαρμογή).\n\n Αν θεωρείτε ότι πρέπει να πληρώνουμε το mapbox (η να αλλάξουμε πάροχο χαρτών), παρακαλώ δημοσιεύστε στο meshtastic.discourse.group - Λείπει μια απαιτούμενη άδεια, Meshtastic δεν θα λειτοργεί σωστά. Ενεργοποιήστε τις ρυθμίσεις εφαρμογής Android. - Radio σε κατάσταση ύπνου, δεν γίνεται αλλαγή καναλιού - Αναφορά Bug - Αναφέρετε ένα bug - Είστε σίγουροι ότι θέλετε να αναφέρετε ένα bug? Μετά την αναφορά δημοσιεύστε στο meshtastic.discourse.group ώστε να συνδέσουμε την αναφορά με το συμβάν. - Αναφορά - Επιλογή radio - Έχετε κάνει pair με radio %s - Δεν έχετε κάνει pair με radio ακόμη. - Αλλαγή radio - Παρακαλώ κάντε pair μια συσκευή στις ρυθμίσεις Android. - Η διαδικασία pairing ολοκληρώθηκε, εκκίνηση υπηρεσίας - Η διαδικασία pairing απέτυχε, παρακαλώ επιλέξτε πάλι - Ο εντοπισμός τοποθεσίας είναι απενεργοποιημένος, δε μπορούμε να μοιραστούμε τη θέση σας με το mesh. - Κοινοποίηση - Αποσυνδεδεμένο - Συσκευή σε ύπνωση - - Συνδεδεμένος: %s από %s online - + Συνδεδεμένος: %1$s από %2$s online Λίστα κόμβων δικτύου - Αναβάθμιση Firmware - Συνδεδεμένο στο radio - Συνδεδεμένο στο radio (%s) - Αποσυνδεδεμένο, επιλέξτε radio - Συνδεδεμένο στο radio, αλλά βρίσκεται σε ύπνωση - Αναβάθμιση σε %s - Εφαρμογή πολύ παλαιά - Πρέπει να ενημερώσετε την εφαρμογή μέσω Google Play store (ή Github). Είναι πολύ παλαιά ώστε να συνδεθεί με το radio. - Κανένα (απενεργοποιημένο) - Μικρή εμβέλεια (αλλά γρήγορο) - Μεσαία εμβέλεια (αλλά γρήγορο) - Μεγάλη εμβέλεια (αλλά αργό) - Πολύ μεγάλη εμβέλεια (αλλά αργό) - ΜΗ ΑΝΑΓΝΩΡΙΣΙΜΟ - Ειδοποιήσεις Υπηρεσίας Meshtastic - Πρέπει να ενεργοποιήσετε τις υπηρεσίες εντοπισμού τοποθεσίας στις ρυθμίσεις Android - Σχετικά - Λίστα κόμβων στο mesh - Μηνύματα - Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bdaa4ffea..297425da4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -45,7 +45,7 @@ Compartir Desconectado Dispositivo en reposo - Conectado: %s de %s en línea + Conectado: %1$s de %2$s en línea Una lista de nodos en la red Actualizar el firmware Conectado a la radio diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 98dc00114..14b590c53 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -49,7 +49,7 @@ Partager Déconnecté Appareil en veille - Connecté: %s sur %s en ligne + Connecté: %1$s sur %2$s en ligne Une liste de nœuds dans le réseau Mise à jour du Firmware Connecté à une radio diff --git a/app/src/main/res/values-ht/strings.xml b/app/src/main/res/values-ht/strings.xml index 8c02da04e..9f1fa4f71 100644 --- a/app/src/main/res/values-ht/strings.xml +++ b/app/src/main/res/values-ht/strings.xml @@ -46,7 +46,7 @@ Pataje Dekonekte Aparèy ap dòmi - Konekte: %s nan %s disponib + Konekte: %1$s nan %2$s disponib Yon lis ne elektwonik nan rezo a Mete ajou mikrolojisyèl Konekte ak radyo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2b567d09c..ab0c66d30 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -47,7 +47,7 @@ Megosztás Szétkapcsolva Az eszköz alszik - Kapcsolódva: %s a %s-ból(ből) elérhető + Kapcsolódva: %1$s a %2$s-ból(ből) elérhető Hálózati állomások listája Firmware frissítés Kapcsolódva a rádióhoz diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index bc1d039f6..b38ebb5aa 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -48,7 +48,7 @@ mapboxの有償プラン(または代替地図プロバイダ)を検討さ シェア 切断 スリープ - 接続済み:%s人オンライン%s人中 + 接続済み:%1$s人オンライン%2$s人中 ネットワーク内のノードリスト ファームウェアアップデート Meshtasticデバイスに接続しました。(%s) diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index b624cf4a3..cad8b33c9 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -46,7 +46,7 @@ 공유 연결 해제 장치 잠자기 - 연결: %s 온라인( 전체 %s) + 연결: %1$s 온라인( 전체 %2$s) 네트워크안은 모든 노드의 목록 펌웨어 업데이트 라디오로 연결됨 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3df9b39ed..fb4323827 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -48,7 +48,7 @@ Deel Niet verbonden Apparaat in slaapstand - Verbonden: %s van %s online + Verbonden: %1$s van %2$s online Een lijst van de aansluitpunten in het netwerk Programma Updaten Verbonden met een radio diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index e17082877..437e1d080 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -48,7 +48,7 @@ Del Frakoblet Enhet sover - Tilkoblet: %s av %s på nett + Tilkoblet: %1$s av %2$s på nett En liste over noder i nettverket Oppdater Firmware Tilkoblet radio diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d5e0e74c0..5b9bb650e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -53,7 +53,7 @@ Udostępnij Rozłączone Urządzenie uśpione. - Połączono: %s of %s online + Połączono: %1$s of %2$s online Lista użytkowników w sieci Aktualizuj oprogramowanie. Połączony z urządzeniem diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 985b0106e..b2544e4e2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -48,7 +48,7 @@ Compartilhar Desconectado Dispositivo em suspensão (sleep) - Conectado: %s de %s online + Conectado: %1$s de %2$s online Lista de dispositivos na rede Atualizar Firmware Conectado ao rádio diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index fc4bdd4bd..5a60314ce 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -47,7 +47,7 @@ Partilha Desconectado Dispositivo a dormir - Conectado: %s de %s online + Conectado: %1$s de %2$s online Lista de nós na rede Atualizar Firmware Conectado ao rádio diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ca6d109dd..bd51f8773 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -48,7 +48,7 @@ Distribuie Deconectat Dispozitiv în sleep mode - Connectat: %s din %s online + Connectat: %1$s din %2$s online O lista cu nodurile din rețea Updateaza firmware-ul Connectat la dispozitiv diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 43e8cc17e..cbeabe0a3 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -48,7 +48,7 @@ Zdieľať Odpojené Vysielač uspatý - Pripojený: %s z %s je online + Pripojený: %1$s z %2$s je online Zoznam vysielačov v sieti Aktualizácia firmvéru Pripojené k vysielaču diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index aaffb5498..a5fc7d495 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -46,7 +46,7 @@ Deliti Prekinjeno Naprava je v "spanju" - Povezano: %s od %s je na mreži + Povezano: %1$s od %2$s je na mreži Seznam vozlišč v omrežju Posodobite vdelano programsko opremo Povezana z radiem diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 811ecab89..3d2b68d3e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -48,7 +48,7 @@ Paylaş Bağlantı sonlandı Cihaz uyku durumunda - Bağlandı: %s / %s online + Bağlandı: %1$s / %2$s online Ağdaki node listesi Yazılım güncelle Radyoya bağlandı diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 37137fb43..a456fb349 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -48,7 +48,7 @@ 分享 断开连接 设备休眠中 - 连接: %s 中 %s 在线 + 连接: %1$s 中 %2$s 在线 网络中节点列表 更新固件 连接设备 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58b289b36..38c4573a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,7 +52,7 @@ Share Disconnected Device sleeping - Connected: %s of %s online + Connected: %1$s of %2$s online A list of nodes in the network Update Firmware Connected to radio From 52f7a862b39ea5acde93cc3fdc07cbf58cc6d4f6 Mon Sep 17 00:00:00 2001 From: andrekir Date: Wed, 2 Mar 2022 17:27:32 -0300 Subject: [PATCH 6/7] update gradle and libs --- app/build.gradle | 4 ++-- build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9dcd3403e..9ec3d46ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -122,7 +122,7 @@ protobuf { dependencies { - def room_version = '2.4.1' + def room_version = '2.4.2' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.4.1' @@ -134,7 +134,7 @@ dependencies { implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation "androidx.room:room-runtime:$room_version" implementation "com.google.dagger:hilt-android:$hilt_version" kapt "androidx.room:room-compiler:$room_version" diff --git a/build.gradle b/build.gradle index c4e902af7..49310ac9f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '1.6.10' - ext.coroutines_version = "1.5.2" + ext.coroutines_version = "1.6.0" ext.hilt_version = '2.40.5' repositories { @@ -10,7 +10,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" From 91b2767634f895104296adce8856ddc297add748 Mon Sep 17 00:00:00 2001 From: andrekir Date: Wed, 2 Mar 2022 17:28:18 -0300 Subject: [PATCH 7/7] 1.2.59 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9ec3d46ad..e038e8315 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,8 +43,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added - versionCode 20258 // format is Mmmss (where M is 1+the numeric major number - versionName "1.2.58" + versionCode 20259 // format is Mmmss (where M is 1+the numeric major number + versionName "1.2.59" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // per https://developer.android.com/studio/write/vector-asset-studio