Compare commits

...

12 commits
main ... 1.2.59

Author SHA1 Message Date
Andre Kirchhoff
4092fc5c7f
Merge pull request #388 from meshtastic/1.2-release
1.2.59
2022-03-02 17:50:24 -03:00
andrekir
91b2767634 1.2.59 2022-03-02 17:28:18 -03:00
andrekir
52f7a862b3 update gradle and libs 2022-03-02 17:27:32 -03:00
andrekir
ede48be4f3 fix "Multiple substitutions specified in non-positional format of string resource string/connected_count" 2022-03-02 14:25:35 -03:00
andrekir
598ec54cf3 anonymize sendPosition 2022-03-02 14:23:41 -03:00
Mike Cumings
49188adc36 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 9592fd68de)
2022-03-02 14:10:01 -03:00
Mike Cumings
c0fe9213f1 Issue #369 - Use repository pattern for bluetooth state
(cherry picked from commit b3878a4240)
2022-03-02 14:10:01 -03:00
Mike Cumings
1294eee8e3 CSV export improvements to make it more reliable
(cherry picked from commit 16d2b2e5f3)
2022-03-02 14:10:01 -03:00
Andre Kirchhoff
5659725b96
Merge pull request #387 from meshtastic/menu-messages
add action mode menu to messages (delete & select all)
2022-03-02 12:12:20 -03:00
andrekir
6ad2b1814f deleteMessage index by packet.id 2022-03-02 11:39:30 -03:00
andrekir
01f8154189 add action mode menu to messages (delete & select all) 2022-03-02 11:39:07 -03:00
Andre Kirchhoff
7395cc5583
tie 1.2-legacy releases to 1.2-legacy firmware 2022-02-28 16:34:55 -03:00
44 changed files with 589 additions and 327 deletions

View file

@ -47,6 +47,7 @@ jobs:
with: with:
repository: meshtastic/Meshtastic-device repository: meshtastic/Meshtastic-device
releases-only: true releases-only: true
prefix: 'v1.2.'
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Create version strings - name: Create version strings

View file

@ -43,8 +43,8 @@ android {
applicationId "com.geeksville.mesh" applicationId "com.geeksville.mesh"
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) 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 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 versionCode 20259 // format is Mmmss (where M is 1+the numeric major number
versionName "1.2.58" versionName "1.2.59"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// per https://developer.android.com/studio/write/vector-asset-studio // per https://developer.android.com/studio/write/vector-asset-studio
@ -122,7 +122,7 @@ protobuf {
dependencies { dependencies {
def room_version = '2.4.1' def room_version = '2.4.2'
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.appcompat:appcompat:1.4.1'
@ -134,7 +134,7 @@ dependencies {
implementation 'com.google.android.material:material:1.5.0' implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.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 "androidx.room:room-runtime:$room_version"
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"

View file

@ -66,8 +66,7 @@ interface IMeshService {
*/ */
void send(inout DataPacket packet); void send(inout DataPacket packet);
void deleteMessage(int packetId);
void delete(int position);
void deleteAllMessages(); void deleteAllMessages();

View file

@ -3,6 +3,9 @@ package com.geeksville.mesh
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -15,4 +18,14 @@ object ApplicationModule {
fun provideSharedPreferences(application: Application): SharedPreferences { fun provideSharedPreferences(application: Application): SharedPreferences {
return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
} }
@Provides
fun provideProcessLifecycleOwner(): LifecycleOwner {
return ProcessLifecycleOwner.get()
}
@Provides
fun provideProcessLifecycle(processLifecycleOwner: LifecycleOwner): Lifecycle {
return processLifecycleOwner.lifecycle
}
} }

View file

@ -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
}

View file

@ -4,7 +4,6 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.* import android.content.*
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -40,6 +39,7 @@ import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.android.* import com.geeksville.mesh.android.*
import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
@ -134,11 +134,7 @@ class MainActivity : AppCompatActivity(), Logging,
// Used to schedule a coroutine in the GUI thread // Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope(Dispatchers.Main + Job()) private val mainScope = CoroutineScope(Dispatchers.Main + Job())
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { private val bluetoothViewModel: BluetoothViewModel by viewModels()
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
val model: UIViewModel by viewModels() val model: UIViewModel by viewModels()
data class TabInfo(val text: String, val icon: Int, val content: Fragment) 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 /** Get the minimum permissions our app needs to run correctly
*/ */
private fun getMinimumPermissions(): List<String> { private fun getMinimumPermissions(): List<String> {
@ -381,7 +355,7 @@ class MainActivity : AppCompatActivity(), Logging,
} }
} }
updateBluetoothEnabled() bluetoothViewModel.permissionsUpdated()
} }
@ -445,12 +419,6 @@ class MainActivity : AppCompatActivity(), Logging,
/// Set theme /// Set theme
setUITheme(prefs) setUITheme(prefs)
/// Set initial bluetooth state
updateBluetoothEnabled()
/// We now want to be informed of bluetooth state
registerReceiver(btStateReceiver, btStateReceiver.intentFilter)
/* not yet working /* not yet working
// Configure sign-in to request the user's ID, email address, and basic // Configure sign-in to request the user's ID, email address, and basic
// profile. ID and basic profile are included in DEFAULT_SIGN_IN. // profile. ID and basic profile are included in DEFAULT_SIGN_IN.
@ -569,7 +537,6 @@ class MainActivity : AppCompatActivity(), Logging,
} }
override fun onDestroy() { override fun onDestroy() {
unregisterReceiver(btStateReceiver)
unregisterMeshReceiver() unregisterMeshReceiver()
mainScope.cancel("Activity going away") mainScope.cancel("Activity going away")
super.onDestroy() super.onDestroy()
@ -1003,18 +970,18 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
bluetoothViewModel.enabled.observe(this) { enabled ->
if (!enabled) {
// Ask to start bluetooth if no USB devices are visible // Ask to start bluetooth if no USB devices are visible
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty() val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
if (!isInTestLab && !hasUSB) { if (!isInTestLab && !hasUSB) {
if (hasConnectPermission()) { if (hasConnectPermission()) {
bluetoothAdapter?.let {
if (!it.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
}
} else requestPermission() } else requestPermission()
} }
}
}
try { try {
bindMeshService() bindMeshService()

View file

@ -16,8 +16,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
packetDao.getAllPacket(MAX_ITEMS) packetDao.getAllPacket(MAX_ITEMS)
} }
suspend fun getAllPacketsInReceiveOrder(): Flow<List<Packet>> = withContext(Dispatchers.IO) { suspend fun getAllPacketsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<Packet>> = withContext(Dispatchers.IO) {
packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS) packetDao.getAllPacketsInReceiveOrder(maxItems)
} }
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) { suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {

View file

@ -16,7 +16,7 @@ data class Packet(@PrimaryKey val uuid: String,
@ColumnInfo(name = "message") val raw_message: String @ColumnInfo(name = "message") val raw_message: String
) { ) {
val proto: MeshProtos.MeshPacket? val meshPacket: MeshProtos.MeshPacket?
get() { get() {
if (message_type == "packet") { if (message_type == "packet") {
val builder = MeshProtos.MeshPacket.newBuilder() val builder = MeshProtos.MeshPacket.newBuilder()
@ -28,13 +28,27 @@ data class Packet(@PrimaryKey val uuid: String,
} }
return null 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? val position: MeshProtos.Position?
get() { get() {
return proto?.run { return meshPacket?.run {
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) { if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
return MeshProtos.Position.parseFrom(decoded.payload) return MeshProtos.Position.parseFrom(decoded.payload)
} }
return null return null
} } ?: nodeInfo?.position
} }
} }

View file

@ -0,0 +1,24 @@
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 kotlinx.coroutines.flow.map
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() {
/**
* Called when permissions have been updated. This causes an explicit refresh of the
* bluetooth state.
*/
fun permissionsUpdated() = bluetoothRepository.refreshState()
val enabled = bluetoothRepository.state.map { it.enabled }.asLiveData()
}

View file

@ -95,12 +95,12 @@ class MessagesState(private val ui: UIViewModel) : Logging {
addMessage(p) addMessage(p)
} }
fun deleteMessage(packet: DataPacket, position: Int) { fun deleteMessage(packet: DataPacket) {
val service = ui.meshService val service = ui.meshService
if (service != null) { if (service != null) {
try { try {
service.delete(position) service.deleteMessage(packet.id)
} catch (ex: RemoteException) { } catch (ex: RemoteException) {
packet.errorMessage = "Error: ${ex.message}" packet.errorMessage = "Error: ${ex.message}"
} }
@ -116,7 +116,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
try { try {
service.deleteAllMessages() service.deleteAllMessages()
} catch (ex: RemoteException) { } catch (ex: RemoteException) {
errormsg("Error: ${ex.message}")
} }
removeAllMessages() removeAllMessages()
} }

View file

@ -74,10 +74,6 @@ class UIViewModel @Inject constructor(
debug("ViewModel created") debug("ViewModel created")
} }
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(packet)
}
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) { fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll() repository.deleteAll()
} }
@ -229,10 +225,6 @@ class UIViewModel @Inject constructor(
val ownerName = object : MutableLiveData<String>("MrIDE Test") { val ownerName = object : MutableLiveData<String>("MrIDE Test") {
} }
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
}
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) { val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) {
override fun setValue(value: Boolean) { override fun setValue(value: Boolean) {
super.setValue(value) 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 // clean up all this nasty owner state management FIXME
fun setOwner(s: String? = null) { fun setOwner(s: String? = null) {
@ -283,49 +272,67 @@ class UIViewModel @Inject constructor(
// Capture the current node value while we're still on main thread // Capture the current node value while we're still on main thread
val nodes = nodeDB.nodes.value ?: emptyMap() val nodes = nodeDB.nodes.value ?: emptyMap()
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
meshPosition?.let { Position(it) }.takeIf {
it?.isValid() == true
}
}
writeToUri(file_uri) { writer -> writeToUri(file_uri) { writer ->
// Create a map of nodes keyed by their ID // 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") 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 // Packets are ordered by time, we keep most recent position of
// our device in localNodePosition. // our device in localNodePosition.
var localNodePosition: MeshProtos.Position? = null
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault()) val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
repository.getAllPacketsInReceiveOrder().first().forEach { packet -> repository.getAllPacketsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
packet.proto?.let { proto -> // 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 -> packet.position?.let { position ->
if (proto.from == myNodeNum) { positionToPos.invoke(position)?.let { _ ->
localNodePosition = position nodePositions[proto.from] = position
} else { }
}
// 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 rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt() val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: "" val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long // sender lat & long
val senderPos = packet.position val senderPosition = nodePositions[proto.from]
?.let { p -> Position(p) } val senderPos = positionToPos.invoke(senderPosition)
?.takeIf { p -> p.isValid() }
val senderLat = senderPos?.latitude ?: "" val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: "" val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation // rx lat, long, and elevation
val rxPos = localNodePosition val rxPosition = nodePositions[myNodeNum]
?.let { p -> Position(p) } val rxPos = positionToPos.invoke(rxPosition)
?.takeIf { p -> p.isValid() }
val rxLat = rxPos?.latitude ?: "" val rxLat = rxPos?.latitude ?: ""
val rxLong = rxPos?.longitude ?: "" val rxLong = rxPos?.longitude ?: ""
val rxAlt = rxPos?.altitude ?: "" val rxAlt = rxPos?.altitude ?: ""
val rxSnr = "%f".format(proto.rxSnr) val rxSnr = "%f".format(proto.rxSnr)
// Calculate the distance if both positions are valid // Calculate the distance if both positions are valid
val dist = if (senderPos == null || rxPos == null) { val dist = if (senderPos == null || rxPos == null) {
"" ""
} else { } else {
positionToMeter( positionToMeter(
localNodePosition!!, rxPosition!!, // Use rxPosition but only if rxPos was valid
position senderPosition!! // Use senderPosition but only if senderPos was valid
).roundToInt().toString() ).roundToInt().toString()
} }
@ -347,7 +354,6 @@ class UIViewModel @Inject constructor(
} }
} }
} }
}
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) { private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View file

@ -1,4 +1,4 @@
package com.geeksville.mesh.service package com.geeksville.mesh.repository.bluetooth
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -6,29 +6,26 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import com.geeksville.util.exceptionReporter import com.geeksville.util.exceptionReporter
import javax.inject.Inject
/** /**
* A helper class to call onChanged when bluetooth is enabled or disabled * A helper class to call onChanged when bluetooth is enabled or disabled
*/ */
class BluetoothStateReceiver( class BluetoothBroadcastReceiver @Inject constructor(
private val onChanged: (Boolean) -> Unit private val bluetoothRepository: BluetoothRepository
) : BroadcastReceiver() { ) : BroadcastReceiver() {
internal val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
override fun onReceive(context: Context, intent: Intent) = exceptionReporter { override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) { if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.bluetoothAdapterState) { when (intent.bluetoothAdapterState) {
// Simulate a disconnection if the user disables bluetooth entirely // Simulate a disconnection if the user disables bluetooth entirely
BluetoothAdapter.STATE_OFF -> onChanged(false) BluetoothAdapter.STATE_OFF -> bluetoothRepository.refreshState()
BluetoothAdapter.STATE_ON -> onChanged(true) BluetoothAdapter.STATE_ON -> bluetoothRepository.refreshState()
} }
} }
} }
private val Intent.bluetoothAdapterState: Int private val Intent.bluetoothAdapterState: Int
get() = getIntExtra( get() = getIntExtra(BluetoothAdapter.EXTRA_STATE,-1)
BluetoothAdapter.EXTRA_STATE,
-1
)
} }

View file

@ -0,0 +1,102 @@
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.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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 bluetoothBroadcastReceiverLazy: dagger.Lazy<BluetoothBroadcastReceiver>,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
) : Logging {
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<BluetoothState> = _state.asStateFlow()
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
updateBluetoothState()
bluetoothBroadcastReceiverLazy.get().let { receiver ->
application.registerReceiver(receiver, receiver.intentFilter)
}
}
}
fun refreshState() {
processLifecycle.coroutineScope.launch(dispatchers.default) {
updateBluetoothState()
}
}
fun getRemoteDevice(address: String): BluetoothDevice? {
return bluetoothAdapterLazy.get()?.getRemoteDevice(address)
}
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<Set<BluetoothDevice>>? {
return if (adapter.isEnabled) {
flow<Set<BluetoothDevice>> {
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
}
}

View file

@ -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
}
}
}

View file

@ -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<Set<BluetoothDevice>>? = null
)

View file

@ -1560,7 +1560,7 @@ class MeshService : Service(), Logging {
try { try {
val mi = myNodeInfo val mi = myNodeInfo
if (mi != null) { 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 { val position = MeshProtos.Position.newBuilder().also {
it.longitudeI = Position.degI(lon) it.longitudeI = Position.degI(lon)
@ -1782,10 +1782,12 @@ class MeshService : Service(), Logging {
this@MeshService.setOwner(myId, longName, shortName) this@MeshService.setOwner(myId, longName, shortName)
} }
override fun delete(position: Int) { override fun deleteMessage(packetId: Int) {
if (position >= 0) { val packet = recentDataPackets.find {it.id == packetId}
recentDataPackets.removeAt(position) if (packet != null) {
} recentDataPackets.remove(packet)
debug("Deleting message id=${packet.id}")
} else debug("Nothing to delete, message id=${packetId} not found")
} }
override fun deleteAllMessages() { override fun deleteAllMessages() {

View file

@ -2,24 +2,27 @@ package com.geeksville.mesh.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.companion.CompanionDeviceManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.IBinder import android.os.IBinder
import androidx.core.content.edit 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.BinaryLogFile
import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging import com.geeksville.android.Logging
import com.geeksville.concurrent.handledLaunch import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.IRadioInterfaceService import com.geeksville.mesh.IRadioInterfaceService
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.util.anonymize import com.geeksville.util.anonymize
import com.geeksville.util.ignoreException import com.geeksville.util.ignoreException
import com.geeksville.util.toRemoteExceptions import com.geeksville.util.toRemoteExceptions
import kotlinx.coroutines.CoroutineScope import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.cancel import javax.inject.Inject
open class RadioNotConnectedException(message: String = "Not connected to radio") : 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... * 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. * It is designed to be simple so it can be stubbed out with a simulated version as needed.
*/ */
@AndroidEntryPoint
class RadioInterfaceService : Service(), Logging { 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 { companion object : Logging {
/** /**
* The RECEIVED_FROMRADIO * The RECEIVED_FROMRADIO
@ -104,7 +117,7 @@ class RadioInterfaceService : Service(), Logging {
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun getBondedDeviceAddress(context: Context): String? { fun getBondedDeviceAddress(context: Context): String? {
// If the user has unpaired our device, treat things as if we don't have one // 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 /// Interfaces can filter addresses to indicate that address is no longer acceptable
if (address != null) { if (address != null) {
@ -142,16 +155,6 @@ class RadioInterfaceService : Service(), Logging {
/// true if our interface is currently connected to a device /// true if our interface is currently connected to a device
private var isConnected = false 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) { private fun broadcastConnectionChanged(isConnected: Boolean, isPermanent: Boolean) {
debug("Broadcasting connection=$isConnected") debug("Broadcasting connection=$isConnected")
val intent = Intent(RADIO_CONNECTED_ACTION) val intent = Intent(RADIO_CONNECTED_ACTION)
@ -197,19 +200,35 @@ class RadioInterfaceService : Service(), Logging {
override fun onCreate() { override fun onCreate() {
runningService = this runningService = this
lifecycleDispatcher.onServicePreSuperOnCreate()
super.onCreate() super.onCreate()
registerReceiver(bluetoothStateReceiver, bluetoothStateReceiver.intentFilter)
lifecycleOwner.lifecycle.coroutineScope.launch {
bluetoothRepository.state.collect { state ->
if (state.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() { override fun onDestroy() {
unregisterReceiver(bluetoothStateReceiver)
stopInterface() stopInterface()
serviceScope.cancel("Destroying RadioInterface") serviceScope.cancel("Destroying RadioInterface")
runningService = null runningService = null
lifecycleDispatcher.onServicePreSuperOnDestroy()
super.onDestroy() super.onDestroy()
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
lifecycleDispatcher.onServicePreSuperOnBind()
return binder return binder
} }

View file

@ -1,12 +1,10 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.app.AlertDialog
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.LayoutInflater import android.view.*
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
@ -14,11 +12,11 @@ import android.widget.TextView
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
@ -26,6 +24,7 @@ import com.geeksville.mesh.databinding.MessagesFragmentBinding
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@ -37,7 +36,6 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
if (actionId == receivedActionId) { if (actionId == receivedActionId) {
func() func()
} }
true true
} }
} }
@ -45,6 +43,7 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
@AndroidEntryPoint @AndroidEntryPoint
class MessagesFragment : ScreenFragment("Messages"), Logging { class MessagesFragment : ScreenFragment("Messages"), Logging {
private var actionMode: ActionMode? = null
private var _binding: MessagesFragmentBinding? = null private var _binding: MessagesFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView. // This property is only valid between onCreateView and onDestroyView.
@ -53,7 +52,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
private val model: UIViewModel by activityViewModels() private val model: UIViewModel by activityViewModels()
// Allows textMultiline with IME_ACTION_SEND // Allows textMultiline with IME_ACTION_SEND
fun EditText.onActionSend(func: () -> Unit) { private fun EditText.onActionSend(func: () -> Unit) {
setImeOptions(EditorInfo.IME_ACTION_SEND) setImeOptions(EditorInfo.IME_ACTION_SEND)
setRawInputType(InputType.TYPE_CLASS_TEXT) setRawInputType(InputType.TYPE_CLASS_TEXT)
setOnEditorActionListener { _, actionId, _ -> setOnEditorActionListener { _, actionId, _ ->
@ -61,7 +60,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
if (actionId == EditorInfo.IME_ACTION_SEND) { if (actionId == EditorInfo.IME_ACTION_SEND) {
func() func()
} }
true true
} }
} }
@ -73,22 +71,21 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
private fun getShortDateTime(time: Date): String { private fun getShortDateTime(time: Date): String {
// return time if within 24 hours, otherwise date/time // return time if within 24 hours, otherwise date/time
val one_day = 60 * 60 * 24 * 1000 val oneDayMsec = 60 * 60 * 24 * 1000L
if (System.currentTimeMillis() - time.time > one_day) { return if (System.currentTimeMillis() - time.time > oneDayMsec) {
return dateTimeFormat.format(time) dateTimeFormat.format(time)
} else return timeFormat.format(time) } else timeFormat.format(time)
} }
// Provide a direct reference to each of the views within a data item // Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access // Used to cache the views within the item layout for fast access
class ViewHolder(itemView: AdapterMessageLayoutBinding) : class ViewHolder(itemView: AdapterMessageLayoutBinding) :
RecyclerView.ViewHolder(itemView.root) { RecyclerView.ViewHolder(itemView.root) {
val card: CardView = itemView.Card
val username: Chip = itemView.username val username: Chip = itemView.username
val messageText: TextView = itemView.messageText val messageText: TextView = itemView.messageText
val messageTime: TextView = itemView.messageTime val messageTime: TextView = itemView.messageTime
val messageStatusIcon: ImageView = itemView.messageStatusIcon val messageStatusIcon: ImageView = itemView.messageStatusIcon
val card: CardView = itemView.Card
} }
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() { private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
@ -119,8 +116,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext()) val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
// Inflate the custom layout // Inflate the custom layout
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false) val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
@ -128,6 +123,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
return ViewHolder(contactViewBinding) return ViewHolder(contactViewBinding)
} }
var messages = arrayOf<DataPacket>()
var selectedList = ArrayList<DataPacket>()
/** /**
* Returns the total number of items in the data set held by the adapter. * Returns the total number of items in the data set held by the adapter.
* *
@ -159,7 +157,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val msg = messages[position] val msg = messages[position]
val nodes = model.nodeDB.nodes.value!! val nodes = model.nodeDB.nodes.value!!
val node = nodes.get(msg.from)
// Determine if this is my message (originated on this device). // Determine if this is my message (originated on this device).
// val isMe = model.myNodeInfo.value?.myNodeNum == node?.num // val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
val isMe = msg.from == "^local" val isMe = msg.from == "^local"
@ -167,48 +165,19 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// Set cardview offset and color. // Set cardview offset and color.
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset) val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
holder.card.setOnLongClickListener {
val deleteMessageDialog = AlertDialog.Builder(context)
deleteMessageDialog.setMessage(R.string.delete_selected_message)
deleteMessageDialog.setPositiveButton(
R.string.delete
) { _, _ ->
model.messagesState.deleteMessage((messages[position]), position)
}
deleteMessageDialog.setNeutralButton(
R.string.cancel
) { _, _ ->
}
deleteMessageDialog.setNegativeButton(
R.string.delete_all_messages
) { _, _ ->
model.messagesState.deleteAllMessages()
}
deleteMessageDialog.create()
deleteMessageDialog.show()
true
}
if (isMe) { if (isMe) {
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
marginParams.leftMargin = messageOffset marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0 marginParams.rightMargin = 0
context?.let { context?.let {
holder.card.setCardBackgroundColor( holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg))
ContextCompat.getColor(
it,
R.color.colorMyMsg
)
)
} }
} else { } else {
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
marginParams.rightMargin = messageOffset marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0 marginParams.leftMargin = 0
context?.let { context?.let {
holder.card.setCardBackgroundColor( holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg))
ContextCompat.getColor(
it,
R.color.colorMsg
)
)
} }
} }
// Hide the username chip for my messages // Hide the username chip for my messages
@ -217,11 +186,14 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
} else { } else {
holder.username.visibility = View.VISIBLE holder.username.visibility = View.VISIBLE
// If we can't find the sender, just use the ID // If we can't find the sender, just use the ID
val node = nodes[msg.from]
val user = node?.user val user = node?.user
holder.username.text = user?.shortName ?: msg.from holder.username.text = user?.shortName ?: msg.from
} }
if (msg.errorMessage != null) { if (msg.errorMessage != null) {
context?.let { holder.card.setCardBackgroundColor(Color.RED) } holder.itemView.context?.let {
holder.card.setCardBackgroundColor(Color.RED)
}
holder.messageText.text = msg.errorMessage holder.messageText.text = msg.errorMessage
} else { } else {
holder.messageText.text = msg.text holder.messageText.text = msg.text
@ -243,9 +215,114 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
} else } else
holder.messageStatusIcon.visibility = View.INVISIBLE holder.messageStatusIcon.visibility = View.INVISIBLE
holder.itemView.setOnLongClickListener {
if (actionMode == null) {
actionMode = (activity as MainActivity).startActionMode(object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.menu_messages, menu)
mode.title = "1"
return true
} }
private var messages = arrayOf<DataPacket>() override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
clickItem(holder)
return true
}
override fun onActionItemClicked(
mode: ActionMode,
item: MenuItem
): Boolean {
when (item.itemId) {
R.id.deleteButton -> {
val deleteMessagesString = resources.getQuantityString(
R.plurals.delete_messages,
selectedList.size,
selectedList.size
)
MaterialAlertDialogBuilder(requireContext())
.setMessage(deleteMessagesString)
.setPositiveButton(getString(R.string.delete)) { _, _ ->
debug("User clicked deleteButton")
// all items selected --> deleteAllMessages()
if (selectedList.size == messages.size) {
model.messagesState.deleteAllMessages()
} else {
selectedList.forEach {
model.messagesState.deleteMessage(it)
}
mode.finish()
}
}
.setNeutralButton(R.string.cancel) { _, _ ->
}
.show()
}
R.id.selectAllButton -> {
// if all selected -> unselect all
if (selectedList.size == messages.size) {
selectedList.clear()
mode.finish()
} else {
// else --> select all
selectedList.clear()
selectedList.addAll(messages)
}
actionMode?.title = selectedList.size.toString()
notifyDataSetChanged()
}
}
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
selectedList.clear()
notifyDataSetChanged()
actionMode = null
}
})
} else {
// when action mode is enabled
clickItem(holder)
}
true
}
holder.itemView.setOnClickListener {
if (actionMode != null) clickItem(holder)
}
if (selectedList.contains(msg)) {
holder.itemView.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 32f
setColor(Color.rgb(127, 127, 127))
}
} else {
holder.itemView.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 32f
setColor(ContextCompat.getColor(holder.itemView.context, R.color.colorAdvancedBackground))
}
}
}
private fun clickItem(holder: ViewHolder) {
val position = holder.bindingAdapterPosition
if (!selectedList.contains(messages[position])) {
selectedList.add(messages[position])
} else {
selectedList.remove(messages[position])
}
if (selectedList.isEmpty()) {
// finish action mode when no items selected
actionMode?.finish()
} else {
// show total items selected on action mode title
actionMode?.title = "${selectedList.size}"
}
notifyItemChanged(position)
}
/// Called when our node DB changes /// Called when our node DB changes
fun onMessagesChanged(msgIn: Collection<DataPacket>) { fun onMessagesChanged(msgIn: Collection<DataPacket>) {
@ -258,10 +335,15 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
} }
} }
override fun onPause() {
actionMode?.finish()
super.onPause()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
_binding = MessagesFragmentBinding.inflate(inflater, container, false) _binding = MessagesFragmentBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -269,7 +351,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.sendButton.setOnClickListener { binding.sendButton.setOnClickListener {
debug("sendButton click") debug("User clicked sendButton")
val str = binding.messageInputText.text.toString().trim() val str = binding.messageInputText.text.toString().trim()
if (str.isNotEmpty()) if (str.isNotEmpty())
@ -295,34 +377,20 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
layoutManager.stackFromEnd = true // We want the last rows to always be shown layoutManager.stackFromEnd = true // We want the last rows to always be shown
binding.messageListView.layoutManager = layoutManager binding.messageListView.layoutManager = layoutManager
model.messagesState.messages.observe(viewLifecycleOwner, Observer { model.messagesState.messages.observe(viewLifecycleOwner) {
debug("New messages received: ${it.size}") debug("New messages received: ${it.size}")
messagesAdapter.onMessagesChanged(it) messagesAdapter.onMessagesChanged(it)
}) }
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages // If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
fun updateTextEnabled() { model.isConnected.observe(viewLifecycleOwner) { connectionState ->
binding.textInputLayout.isEnabled = // If we don't know our node ID and we are offline don't let user try to send
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED val connected = connectionState == MeshService.ConnectionState.CONNECTED
binding.textInputLayout.isEnabled = connected
binding.sendButton.isEnabled = connected
// Just being connected is enough to allow sending texts I think // Just being connected is enough to allow sending texts I think
// && model.nodeDB.myId.value != null && model.radioConfig.value != null // && model.nodeDB.myId.value != null && model.radioConfig.value != null
} }
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
updateTextEnabled()
})
/* model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
updateTextEnabled()
})
model.radioConfig.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
updateTextEnabled()
}) */
} }
} }

View file

@ -33,6 +33,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.RadioConfigProtos import com.geeksville.mesh.RadioConfigProtos
import com.geeksville.mesh.android.* import com.geeksville.mesh.android.*
import com.geeksville.mesh.databinding.SettingsFragmentBinding import com.geeksville.mesh.databinding.SettingsFragmentBinding
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.* import com.geeksville.mesh.service.*
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS 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 binding get() = _binding!!
private val scanModel: BTScanModel by activityViewModels() private val scanModel: BTScanModel by activityViewModels()
private val bluetoothViewModel: BluetoothViewModel by activityViewModels()
private val model: UIViewModel by activityViewModels() private val model: UIViewModel by activityViewModels()
// FIXME - move this into a standard GUI helper class // 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) regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = regionAdapter spinner.adapter = regionAdapter
model.bluetoothEnabled.observe(viewLifecycleOwner) { bluetoothViewModel.enabled.observe(viewLifecycleOwner) {
if (it) binding.changeRadioButton.show() if (it) binding.changeRadioButton.show()
else binding.changeRadioButton.hide() else binding.changeRadioButton.hide()
} }
@ -813,7 +815,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) { if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) {
binding.warningNotPaired.visibility = View.GONE binding.warningNotPaired.visibility = View.GONE
// binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio) // 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.warningNotPaired.visibility = View.VISIBLE
binding.scanStatusText.text = getString(R.string.not_paired_yet) binding.scanStatusText.text = getString(R.string.not_paired_yet)
} }

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,9h8v10H8z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
</vector>

View file

@ -42,7 +42,6 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:autoLink="all" android:autoLink="all"
android:text="@string/sample_message" android:text="@string/sample_message"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/username" app:layout_constraintStart_toEndOf="@id/username"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/deleteButton"
android:icon="@drawable/ic_twotone_delete_24"
android:title="@string/delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/selectAllButton"
android:icon="@drawable/ic_twotone_select_all_24"
android:title="@string/select_all"
app:showAsAction="ifRoom" />
</menu>

View file

@ -48,7 +48,7 @@
<string name="share">Sdílet</string> <string name="share">Sdílet</string>
<string name="disconnected">Odpojeno</string> <string name="disconnected">Odpojeno</string>
<string name="device_sleeping">Zařízení spí</string> <string name="device_sleeping">Zařízení spí</string>
<string name="connected_count">Pripojeno: %s z %s je online</string> <string name="connected_count">Pripojeno: %1$s z %2$s je online</string>
<string name="list_of_nodes">Seznam vysílačů v síti</string> <string name="list_of_nodes">Seznam vysílačů v síti</string>
<string name="update_firmware">Aktualizace softwaru</string> <string name="update_firmware">Aktualizace softwaru</string>
<string name="connected_to">Připojeno k vysílači (%s)</string> <string name="connected_to">Připojeno k vysílači (%s)</string>

View file

@ -1,142 +1,73 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="action_settings">Ρυθμίσεις</string> <string name="action_settings">Ρυθμίσεις</string>
<string name="channel_name">Όνομα Καναλιού</string> <string name="channel_name">Όνομα Καναλιού</string>
<string name="channel_options">Επιλογές Καναλιού</string> <string name="channel_options">Επιλογές Καναλιού</string>
<string name="share_button">Κοινή χρήση</string> <string name="share_button">Κοινή χρήση</string>
<string name="qr_code">Κώδικας QR</string> <string name="qr_code">Κώδικας QR</string>
<string name="unset">Αναίρεση</string> <string name="unset">Αναίρεση</string>
<string name="connection_status">Κατάσταση Σύνδεσης</string> <string name="connection_status">Κατάσταση Σύνδεσης</string>
<string name="application_icon">Εικονίδιο εφαρμογής </string> <string name="application_icon">Εικονίδιο εφαρμογής </string>
<string name="unknown_username">Άγνωστο Όνομα Χρήστη</string> <string name="unknown_username">Άγνωστο Όνομα Χρήστη</string>
<string name="user_avatar">Avatar Χρήστη</string> <string name="user_avatar">Avatar Χρήστη</string>
<string name="sample_message">Βρήκα το σημείο, είναι διπλα στη τίγρη. Φοβάμαι λίγο.</string> <string name="sample_message">Βρήκα το σημείο, είναι διπλα στη τίγρη. Φοβάμαι λίγο.</string>
<string name="send_text">Αποστολή κειμένου</string> <string name="send_text">Αποστολή κειμένου</string>
<string name="warning_not_paired">Δεν έχετε κάνει ακόμη pair μια συσκευή συμβατή με Meshtastic με το τηλέφωνο. Παρακαλώ κάντε pair μια συσκευή και ορίστε το όνομα χρήστη.\n\nΗ εφαρμογή ανοιχτού κώδικα βρίσκεται σε alpha-testing, αν εντοπίσετε προβλήματα παρακαλώ δημοσιεύστε τα στο forum: meshtastic.discourse.group.\n\nΠερισσότερες πληροφορίες στην ιστοσελίδα - www.meshtastic.org.</string> <string name="warning_not_paired">Δεν έχετε κάνει ακόμη pair μια συσκευή συμβατή με Meshtastic με το τηλέφωνο. Παρακαλώ κάντε pair μια συσκευή και ορίστε το όνομα χρήστη.\n\nΗ εφαρμογή ανοιχτού κώδικα βρίσκεται σε alpha-testing, αν εντοπίσετε προβλήματα παρακαλώ δημοσιεύστε τα στο forum: meshtastic.discourse.group.\n\nΠερισσότερες πληροφορίες στην ιστοσελίδα - www.meshtastic.org.</string>
<string name="username_unset">Όνομα Χρήστη αναιρέθηκε</string> <string name="username_unset">Όνομα Χρήστη αναιρέθηκε</string>
<string name="your_name">Όνομα</string> <string name="your_name">Όνομα</string>
<string name="analytics_okay">Ανώνυμα στατιστικά χρήσης και αναφορές crash.</string> <string name="analytics_okay">Ανώνυμα στατιστικά χρήσης και αναφορές crash.</string>
<string name="looking_for_meshtastic_devices">Αναζήτηση συσκευών Meshtastic …</string> <string name="looking_for_meshtastic_devices">Αναζήτηση συσκευών Meshtastic …</string>
<string name="requires_bluetooth">Η εφαρμογή απαιτεί bluetooth πρόσβαση. Παρακαλώ παρέχεται σχετική άδεια χρήσης στις ρυθμίσεις του android.</string> <string name="requires_bluetooth">Η εφαρμογή απαιτεί bluetooth πρόσβαση. Παρακαλώ παρέχεται σχετική άδεια χρήσης στις ρυθμίσεις του android.</string>
<string name="error_bluetooth">Σφάλμα - η εφαρμογή απαιτεί bluetooth</string> <string name="error_bluetooth">Σφάλμα - η εφαρμογή απαιτεί bluetooth</string>
<string name="starting_pairing">Αρχή pairing</string> <string name="starting_pairing">Αρχή pairing</string>
<string name="pairing_failed">Pairing απέτυχε</string> <string name="pairing_failed">Pairing απέτυχε</string>
<string name="url_for_join">Διεύθυνση URL για συμμετοχή σε Meshtastic mesh</string> <string name="url_for_join">Διεύθυνση URL για συμμετοχή σε Meshtastic mesh</string>
<string name="accept">Αποδοχή</string> <string name="accept">Αποδοχή</string>
<string name="cancel">Ακύρωση</string> <string name="cancel">Ακύρωση</string>
<string name="change_channel">Αλλαγή καναλιού</string> <string name="change_channel">Αλλαγή καναλιού</string>
<string name="are_you_sure_channel">Είστε βέβαιοι ότι θέλετε να αλλάξετε κανάλι? Η επικοινωνία με άλλες συσκευές θα σταματήσεις μέχρι να μοιραστείτε τις ρυθμίσεις του νέου καναλιού.</string> <string name="are_you_sure_channel">Είστε βέβαιοι ότι θέλετε να αλλάξετε κανάλι? Η επικοινωνία με άλλες συσκευές θα σταματήσεις μέχρι να μοιραστείτε τις ρυθμίσεις του νέου καναλιού.</string>
<string name="new_channel_rcvd">Λήψη URL νέου καναλιού</string> <string name="new_channel_rcvd">Λήψη URL νέου καναλιού</string>
<string name="do_you_want_switch">Θέλετε να αλλάξετε %s κανάλι?</string> <string name="do_you_want_switch">Θέλετε να αλλάξετε %s κανάλι?</string>
<string name="map_not_allowed">Έχετε απενεργοποιήσει την ανάλυση δεδομένων. Δυστυχώς ο πάροχος χαρτών μας (mapbox) απαιτεί η ανάλυση δεδομένων να επιτρέπεται στο ‘δωρεάν’ πακέτο. Επομένως έχουμε απενεργοποιήσει το χάρτη.\n\n <string name="map_not_allowed">Έχετε απενεργοποιήσει την ανάλυση δεδομένων. Δυστυχώς ο πάροχος χαρτών μας (mapbox) απαιτεί η ανάλυση δεδομένων να επιτρέπεται στο ‘δωρεάν’ πακέτο. Επομένως έχουμε απενεργοποιήσει το χάρτη.\n\n
Αν επιθυμείτε να δείτε το χάρτη, θα πρέπει να ενεργοποιήσετε την ανάλυση δεδομένων στις Ρυθμίσεις (επίσης - προσωρινά - θα πρέπει να επανεκκινήσετε την εφαρμογή).\n\n Αν επιθυμείτε να δείτε το χάρτη, θα πρέπει να ενεργοποιήσετε την ανάλυση δεδομένων στις Ρυθμίσεις (επίσης - προσωρινά - θα πρέπει να επανεκκινήσετε την εφαρμογή).\n\n
Αν θεωρείτε ότι πρέπει να πληρώνουμε το mapbox (η να αλλάξουμε πάροχο χαρτών), παρακαλώ δημοσιεύστε στο meshtastic.discourse.group</string> Αν θεωρείτε ότι πρέπει να πληρώνουμε το mapbox (η να αλλάξουμε πάροχο χαρτών), παρακαλώ δημοσιεύστε στο meshtastic.discourse.group</string>
<string name="permission_missing">Λείπει μια απαιτούμενη άδεια, Meshtastic δεν θα λειτοργεί σωστά. Ενεργοποιήστε τις ρυθμίσεις εφαρμογής Android.</string> <string name="permission_missing">Λείπει μια απαιτούμενη άδεια, Meshtastic δεν θα λειτοργεί σωστά. Ενεργοποιήστε τις ρυθμίσεις εφαρμογής Android.</string>
<string name="radio_sleeping">Radio σε κατάσταση ύπνου, δεν γίνεται αλλαγή καναλιού</string> <string name="radio_sleeping">Radio σε κατάσταση ύπνου, δεν γίνεται αλλαγή καναλιού</string>
<string name="report_bug">Αναφορά Bug</string> <string name="report_bug">Αναφορά Bug</string>
<string name="report_a_bug">Αναφέρετε ένα bug</string> <string name="report_a_bug">Αναφέρετε ένα bug</string>
<string name="report_bug_text">Είστε σίγουροι ότι θέλετε να αναφέρετε ένα bug? Μετά την αναφορά δημοσιεύστε στο meshtastic.discourse.group ώστε να συνδέσουμε την αναφορά με το συμβάν.</string> <string name="report_bug_text">Είστε σίγουροι ότι θέλετε να αναφέρετε ένα bug? Μετά την αναφορά δημοσιεύστε στο meshtastic.discourse.group ώστε να συνδέσουμε την αναφορά με το συμβάν.</string>
<string name="report">Αναφορά</string> <string name="report">Αναφορά</string>
<string name="select_radio">Επιλογή radio</string> <string name="select_radio">Επιλογή radio</string>
<string name="current_pair">Έχετε κάνει pair με radio %s</string> <string name="current_pair">Έχετε κάνει pair με radio %s</string>
<string name="not_paired_yet">Δεν έχετε κάνει pair με radio ακόμη.</string> <string name="not_paired_yet">Δεν έχετε κάνει pair με radio ακόμη.</string>
<string name="change_radio">Αλλαγή radio</string> <string name="change_radio">Αλλαγή radio</string>
<string name="please_pair">Παρακαλώ κάντε pair μια συσκευή στις ρυθμίσεις Android.</string> <string name="please_pair">Παρακαλώ κάντε pair μια συσκευή στις ρυθμίσεις Android.</string>
<string name="pairing_completed">Η διαδικασία pairing ολοκληρώθηκε, εκκίνηση υπηρεσίας</string> <string name="pairing_completed">Η διαδικασία pairing ολοκληρώθηκε, εκκίνηση υπηρεσίας</string>
<string name="pairing_failed_try_again">Η διαδικασία pairing απέτυχε, παρακαλώ επιλέξτε πάλι</string> <string name="pairing_failed_try_again">Η διαδικασία pairing απέτυχε, παρακαλώ επιλέξτε πάλι</string>
<string name="location_disabled">Ο εντοπισμός τοποθεσίας είναι απενεργοποιημένος, δε μπορούμε να μοιραστούμε τη θέση σας με το mesh.</string> <string name="location_disabled">Ο εντοπισμός τοποθεσίας είναι απενεργοποιημένος, δε μπορούμε να μοιραστούμε τη θέση σας με το mesh.</string>
<string name="share">Κοινοποίηση</string> <string name="share">Κοινοποίηση</string>
<string name="disconnected">Αποσυνδεδεμένο</string> <string name="disconnected">Αποσυνδεδεμένο</string>
<string name="device_sleeping">Συσκευή σε ύπνωση</string> <string name="device_sleeping">Συσκευή σε ύπνωση</string>
<string name="connected_count">Συνδεδεμένος: %1$s από %2$s online</string>
<string name="connected_count">Συνδεδεμένος: %s από %s online</string>
<string name="list_of_nodes">Λίστα κόμβων δικτύου</string> <string name="list_of_nodes">Λίστα κόμβων δικτύου</string>
<string name="update_firmware">Αναβάθμιση Firmware</string> <string name="update_firmware">Αναβάθμιση Firmware</string>
<string name="connected">Συνδεδεμένο στο radio</string> <string name="connected">Συνδεδεμένο στο radio</string>
<string name="connected_to">Συνδεδεμένο στο radio (%s)</string> <string name="connected_to">Συνδεδεμένο στο radio (%s)</string>
<string name="not_connected">Αποσυνδεδεμένο, επιλέξτε radio </string> <string name="not_connected">Αποσυνδεδεμένο, επιλέξτε radio </string>
<string name="connected_sleeping">Συνδεδεμένο στο radio, αλλά βρίσκεται σε ύπνωση</string> <string name="connected_sleeping">Συνδεδεμένο στο radio, αλλά βρίσκεται σε ύπνωση</string>
<string name="update_to">Αναβάθμιση σε %s</string> <string name="update_to">Αναβάθμιση σε %s</string>
<string name="app_too_old">Εφαρμογή πολύ παλαιά</string> <string name="app_too_old">Εφαρμογή πολύ παλαιά</string>
<string name="must_update">Πρέπει να ενημερώσετε την εφαρμογή μέσω Google Play store (ή Github). Είναι πολύ παλαιά ώστε να συνδεθεί με το radio.</string> <string name="must_update">Πρέπει να ενημερώσετε την εφαρμογή μέσω Google Play store (ή Github). Είναι πολύ παλαιά ώστε να συνδεθεί με το radio.</string>
<string name="none">Κανένα (απενεργοποιημένο)</string> <string name="none">Κανένα (απενεργοποιημένο)</string>
<string name="modem_config_short">Μικρή εμβέλεια (αλλά γρήγορο)</string> <string name="modem_config_short">Μικρή εμβέλεια (αλλά γρήγορο)</string>
<string name="modem_config_medium">Μεσαία εμβέλεια (αλλά γρήγορο)</string> <string name="modem_config_medium">Μεσαία εμβέλεια (αλλά γρήγορο)</string>
<string name="modem_config_long">Μεγάλη εμβέλεια (αλλά αργό)</string> <string name="modem_config_long">Μεγάλη εμβέλεια (αλλά αργό)</string>
<string name="modem_config_very_long">Πολύ μεγάλη εμβέλεια (αλλά αργό)</string> <string name="modem_config_very_long">Πολύ μεγάλη εμβέλεια (αλλά αργό)</string>
<string name="modem_config_unrecognized">ΜΗ ΑΝΑΓΝΩΡΙΣΙΜΟ</string> <string name="modem_config_unrecognized">ΜΗ ΑΝΑΓΝΩΡΙΣΙΜΟ</string>
<string name="meshtastic_service_notifications">Ειδοποιήσεις Υπηρεσίας Meshtastic</string> <string name="meshtastic_service_notifications">Ειδοποιήσεις Υπηρεσίας Meshtastic</string>
<string name="location_disabled_warning">Πρέπει να ενεργοποιήσετε τις υπηρεσίες εντοπισμού τοποθεσίας στις ρυθμίσεις Android</string> <string name="location_disabled_warning">Πρέπει να ενεργοποιήσετε τις υπηρεσίες εντοπισμού τοποθεσίας στις ρυθμίσεις Android</string>
<string name="about">Σχετικά</string> <string name="about">Σχετικά</string>
<string name="a_list_of_nodes_in_the_mesh">Λίστα κόμβων στο mesh</string> <string name="a_list_of_nodes_in_the_mesh">Λίστα κόμβων στο mesh</string>
<string name="text_messages">Μηνύματα</string> <string name="text_messages">Μηνύματα</string>
<string name="channel_invalid">Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί</string> <string name="channel_invalid">Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί</string>
</resources> </resources>

View file

@ -45,7 +45,7 @@
<string name="share">Compartir</string> <string name="share">Compartir</string>
<string name="disconnected">Desconectado</string> <string name="disconnected">Desconectado</string>
<string name="device_sleeping">Dispositivo en reposo</string> <string name="device_sleeping">Dispositivo en reposo</string>
<string name="connected_count">Conectado: %s de %s en línea</string> <string name="connected_count">Conectado: %1$s de %2$s en línea</string>
<string name="list_of_nodes">Una lista de nodos en la red</string> <string name="list_of_nodes">Una lista de nodos en la red</string>
<string name="update_firmware">Actualizar el firmware</string> <string name="update_firmware">Actualizar el firmware</string>
<string name="connected">Conectado a la radio</string> <string name="connected">Conectado a la radio</string>

View file

@ -49,7 +49,7 @@
<string name="share">Partager</string> <string name="share">Partager</string>
<string name="disconnected">Déconnecté</string> <string name="disconnected">Déconnecté</string>
<string name="device_sleeping">Appareil en veille</string> <string name="device_sleeping">Appareil en veille</string>
<string name="connected_count">Connecté: %s sur %s en ligne</string> <string name="connected_count">Connecté: %1$s sur %2$s en ligne</string>
<string name="list_of_nodes">Une liste de nœuds dans le réseau</string> <string name="list_of_nodes">Une liste de nœuds dans le réseau</string>
<string name="update_firmware">Mise à jour du Firmware</string> <string name="update_firmware">Mise à jour du Firmware</string>
<string name="connected">Connecté à une radio</string> <string name="connected">Connecté à une radio</string>

View file

@ -46,7 +46,7 @@
<string name="share">Pataje</string> <string name="share">Pataje</string>
<string name="disconnected">Dekonekte</string> <string name="disconnected">Dekonekte</string>
<string name="device_sleeping">Aparèy ap dòmi</string> <string name="device_sleeping">Aparèy ap dòmi</string>
<string name="connected_count">Konekte: %s nan %s disponib</string> <string name="connected_count">Konekte: %1$s nan %2$s disponib</string>
<string name="list_of_nodes">Yon lis ne elektwonik nan rezo a</string> <string name="list_of_nodes">Yon lis ne elektwonik nan rezo a</string>
<string name="update_firmware">Mete ajou mikrolojisyèl</string> <string name="update_firmware">Mete ajou mikrolojisyèl</string>
<string name="connected">Konekte ak radyo</string> <string name="connected">Konekte ak radyo</string>

View file

@ -47,7 +47,7 @@
<string name="share">Megosztás</string> <string name="share">Megosztás</string>
<string name="disconnected">Szétkapcsolva</string> <string name="disconnected">Szétkapcsolva</string>
<string name="device_sleeping">Az eszköz alszik</string> <string name="device_sleeping">Az eszköz alszik</string>
<string name="connected_count">Kapcsolódva: %s a %s-ból(ből) elérhető</string> <string name="connected_count">Kapcsolódva: %1$s a %2$s-ból(ből) elérhető</string>
<string name="list_of_nodes">Hálózati állomások listája</string> <string name="list_of_nodes">Hálózati állomások listája</string>
<string name="update_firmware">Firmware frissítés</string> <string name="update_firmware">Firmware frissítés</string>
<string name="connected">Kapcsolódva a rádióhoz</string> <string name="connected">Kapcsolódva a rádióhoz</string>

View file

@ -48,7 +48,7 @@ mapboxの有償プランまたは代替地図プロバイダを検討さ
<string name="share">シェア</string> <string name="share">シェア</string>
<string name="disconnected">切断</string> <string name="disconnected">切断</string>
<string name="device_sleeping">スリープ</string> <string name="device_sleeping">スリープ</string>
<string name="connected_count">接続済み:%s人オンライン%s人中</string> <string name="connected_count">接続済み:%1$s人オンライン%2$s人中</string>
<string name="list_of_nodes">ネットワーク内のノードリスト</string> <string name="list_of_nodes">ネットワーク内のノードリスト</string>
<string name="update_firmware">ファームウェアアップデート</string> <string name="update_firmware">ファームウェアアップデート</string>
<string name="connected_to">Meshtasticデバイスに接続しました。(%s)</string> <string name="connected_to">Meshtasticデバイスに接続しました。(%s)</string>

View file

@ -46,7 +46,7 @@
<string name="share">공유</string> <string name="share">공유</string>
<string name="disconnected">연결 해제</string> <string name="disconnected">연결 해제</string>
<string name="device_sleeping">장치 잠자기</string> <string name="device_sleeping">장치 잠자기</string>
<string name="connected_count">연결: %s 온라인( 전체 %s)</string> <string name="connected_count">연결: %1$s 온라인( 전체 %2$s)</string>
<string name="list_of_nodes">네트워크안은 모든 노드의 목록</string> <string name="list_of_nodes">네트워크안은 모든 노드의 목록</string>
<string name="update_firmware">펌웨어 업데이트</string> <string name="update_firmware">펌웨어 업데이트</string>
<string name="connected">라디오로 연결됨</string> <string name="connected">라디오로 연결됨</string>

View file

@ -48,7 +48,7 @@
<string name="share">Deel</string> <string name="share">Deel</string>
<string name="disconnected">Niet verbonden</string> <string name="disconnected">Niet verbonden</string>
<string name="device_sleeping">Apparaat in slaapstand</string> <string name="device_sleeping">Apparaat in slaapstand</string>
<string name="connected_count">Verbonden: %s van %s online</string> <string name="connected_count">Verbonden: %1$s van %2$s online</string>
<string name="list_of_nodes">Een lijst van de aansluitpunten in het netwerk</string> <string name="list_of_nodes">Een lijst van de aansluitpunten in het netwerk</string>
<string name="update_firmware">Programma Updaten</string> <string name="update_firmware">Programma Updaten</string>
<string name="connected">Verbonden met een radio</string> <string name="connected">Verbonden met een radio</string>

View file

@ -48,7 +48,7 @@
<string name="share">Del</string> <string name="share">Del</string>
<string name="disconnected">Frakoblet</string> <string name="disconnected">Frakoblet</string>
<string name="device_sleeping">Enhet sover</string> <string name="device_sleeping">Enhet sover</string>
<string name="connected_count">Tilkoblet: %s av %s på nett</string> <string name="connected_count">Tilkoblet: %1$s av %2$s på nett</string>
<string name="list_of_nodes">En liste over noder i nettverket</string> <string name="list_of_nodes">En liste over noder i nettverket</string>
<string name="update_firmware">Oppdater Firmware</string> <string name="update_firmware">Oppdater Firmware</string>
<string name="connected">Tilkoblet radio</string> <string name="connected">Tilkoblet radio</string>

View file

@ -53,7 +53,7 @@
<string name="share">Udostępnij</string> <string name="share">Udostępnij</string>
<string name="disconnected">Rozłączone</string> <string name="disconnected">Rozłączone</string>
<string name="device_sleeping">Urządzenie uśpione.</string> <string name="device_sleeping">Urządzenie uśpione.</string>
<string name="connected_count">Połączono: %s of %s online</string> <string name="connected_count">Połączono: %1$s of %2$s online</string>
<string name="list_of_nodes">Lista użytkowników w sieci</string> <string name="list_of_nodes">Lista użytkowników w sieci</string>
<string name="update_firmware">Aktualizuj oprogramowanie.</string> <string name="update_firmware">Aktualizuj oprogramowanie.</string>
<string name="connected">Połączony z urządzeniem</string> <string name="connected">Połączony z urządzeniem</string>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="action_settings">Configurações</string> <string name="action_settings">Configurações</string>
<string name="channel_name">Nome do canal</string> <string name="channel_name">Nome do canal</string>
<string name="channel_options">Opções do canal</string> <string name="channel_options">Opções do canal</string>
@ -48,7 +48,7 @@
<string name="share">Compartilhar</string> <string name="share">Compartilhar</string>
<string name="disconnected">Desconectado</string> <string name="disconnected">Desconectado</string>
<string name="device_sleeping">Dispositivo em suspensão (sleep)</string> <string name="device_sleeping">Dispositivo em suspensão (sleep)</string>
<string name="connected_count">Conectado: %s de %s online</string> <string name="connected_count">Conectado: %1$s de %2$s online</string>
<string name="list_of_nodes">Lista de dispositivos na rede</string> <string name="list_of_nodes">Lista de dispositivos na rede</string>
<string name="update_firmware">Atualizar Firmware</string> <string name="update_firmware">Atualizar Firmware</string>
<string name="connected">Conectado ao rádio</string> <string name="connected">Conectado ao rádio</string>
@ -113,7 +113,13 @@
<string name="allow_will_show">Permitir (exibe diálogo)</string> <string name="allow_will_show">Permitir (exibe diálogo)</string>
<string name="provide_location_to_mesh">Fornecer localização para mesh</string> <string name="provide_location_to_mesh">Fornecer localização para mesh</string>
<string name="camera_required">Permissão da câmera</string> <string name="camera_required">Permissão da câmera</string>
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou video são armazenados.</string> <string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou vídeo são armazenados.</string>
<string name="modem_config_slow_short">Curto alcance / lento</string> <string name="modem_config_slow_short">Curto alcance / lento</string>
<string name="modem_config_slow_medium">Médio alcance / lento</string> <string name="modem_config_slow_medium">Médio alcance / lento</string>
<plurals name="delete_messages">
<item quantity="one" tools:ignore="ImpliedQuantity">Excluir mensagem?</item>
<item quantity="other">Excluir %s mensagens?</item>
</plurals>
<string name="delete">Excluir</string>
<string name="select_all">Selecionar tudo</string>
</resources> </resources>

View file

@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="action_settings">Configurações</string> <string name="action_settings">Configurações</string>
<string name="channel_name">Nome do Canal</string> <string name="channel_name">Nome do Canal</string>
<string name="channel_options">Opções do Canal</string> <string name="channel_options">Opções do Canal</string>
@ -47,7 +47,7 @@
<string name="share">Partilha</string> <string name="share">Partilha</string>
<string name="disconnected">Desconectado</string> <string name="disconnected">Desconectado</string>
<string name="device_sleeping">Dispositivo a dormir</string> <string name="device_sleeping">Dispositivo a dormir</string>
<string name="connected_count">Conectado: %s de %s online</string> <string name="connected_count">Conectado: %1$s de %2$s online</string>
<string name="list_of_nodes">Lista de nós na rede</string> <string name="list_of_nodes">Lista de nós na rede</string>
<string name="update_firmware">Atualizar Firmware</string> <string name="update_firmware">Atualizar Firmware</string>
<string name="connected">Conectado ao rádio</string> <string name="connected">Conectado ao rádio</string>
@ -112,8 +112,14 @@
<string name="cancel_no_radio">Cancelar (sem acesso ao rádio)</string> <string name="cancel_no_radio">Cancelar (sem acesso ao rádio)</string>
<string name="allow_will_show">Permitir (exibe diálogo)</string> <string name="allow_will_show">Permitir (exibe diálogo)</string>
<string name="provide_location_to_mesh">Fornecer localização para mesh</string> <string name="provide_location_to_mesh">Fornecer localização para mesh</string>
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou video são armazenados.</string> <string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou vídeo são armazenados.</string>
<string name="camera_required">Permissão da câmera</string> <string name="camera_required">Permissão da câmera</string>
<string name="modem_config_slow_short">Curto alcance / lento</string> <string name="modem_config_slow_short">Curto alcance / lento</string>
<string name="modem_config_slow_medium">Médio alcance / lento</string> <string name="modem_config_slow_medium">Médio alcance / lento</string>
<plurals name="delete_messages">
<item quantity="one" tools:ignore="ImpliedQuantity">Excluir mensagem?</item>
<item quantity="other">Excluir %s mensagens?</item>
</plurals>
<string name="delete">Excluir</string>
<string name="select_all">Selecionar tudo</string>
</resources> </resources>

View file

@ -48,7 +48,7 @@
<string name="share">Distribuie</string> <string name="share">Distribuie</string>
<string name="disconnected">Deconectat</string> <string name="disconnected">Deconectat</string>
<string name="device_sleeping">Dispozitiv în sleep mode</string> <string name="device_sleeping">Dispozitiv în sleep mode</string>
<string name="connected_count">Connectat: %s din %s online</string> <string name="connected_count">Connectat: %1$s din %2$s online</string>
<string name="list_of_nodes">O lista cu nodurile din rețea</string> <string name="list_of_nodes">O lista cu nodurile din rețea</string>
<string name="update_firmware">Updateaza firmware-ul</string> <string name="update_firmware">Updateaza firmware-ul</string>
<string name="connected">Connectat la dispozitiv</string> <string name="connected">Connectat la dispozitiv</string>

View file

@ -48,7 +48,7 @@
<string name="share">Zdieľať</string> <string name="share">Zdieľať</string>
<string name="disconnected">Odpojené</string> <string name="disconnected">Odpojené</string>
<string name="device_sleeping">Vysielač uspatý</string> <string name="device_sleeping">Vysielač uspatý</string>
<string name="connected_count">Pripojený: %s z %s je online</string> <string name="connected_count">Pripojený: %1$s z %2$s je online</string>
<string name="list_of_nodes">Zoznam vysielačov v sieti</string> <string name="list_of_nodes">Zoznam vysielačov v sieti</string>
<string name="update_firmware">Aktualizácia firmvéru</string> <string name="update_firmware">Aktualizácia firmvéru</string>
<string name="connected">Pripojené k vysielaču</string> <string name="connected">Pripojené k vysielaču</string>

View file

@ -46,7 +46,7 @@
<string name="share">Deliti</string> <string name="share">Deliti</string>
<string name="disconnected">Prekinjeno</string> <string name="disconnected">Prekinjeno</string>
<string name="device_sleeping">Naprava je v "spanju"</string> <string name="device_sleeping">Naprava je v "spanju"</string>
<string name="connected_count">Povezano: %s od %s je na mreži</string> <string name="connected_count">Povezano: %1$s od %2$s je na mreži</string>
<string name="list_of_nodes">Seznam vozlišč v omrežju</string> <string name="list_of_nodes">Seznam vozlišč v omrežju</string>
<string name="update_firmware">Posodobite vdelano programsko opremo</string> <string name="update_firmware">Posodobite vdelano programsko opremo</string>
<string name="connected">Povezana z radiem</string> <string name="connected">Povezana z radiem</string>

View file

@ -48,7 +48,7 @@
<string name="share">Paylaş</string> <string name="share">Paylaş</string>
<string name="disconnected">Bağlantı sonlandı</string> <string name="disconnected">Bağlantı sonlandı</string>
<string name="device_sleeping">Cihaz uyku durumunda</string> <string name="device_sleeping">Cihaz uyku durumunda</string>
<string name="connected_count">Bağlandı: %s / %s online</string> <string name="connected_count">Bağlandı: %1$s / %2$s online</string>
<string name="list_of_nodes">Ağdaki node listesi</string> <string name="list_of_nodes">Ağdaki node listesi</string>
<string name="update_firmware">Yazılım güncelle</string> <string name="update_firmware">Yazılım güncelle</string>
<string name="connected">Radyoya bağlandı</string> <string name="connected">Radyoya bağlandı</string>

View file

@ -48,7 +48,7 @@
<string name="share">分享</string> <string name="share">分享</string>
<string name="disconnected">断开连接</string> <string name="disconnected">断开连接</string>
<string name="device_sleeping">设备休眠中</string> <string name="device_sleeping">设备休眠中</string>
<string name="connected_count">连接: %s 中 %s 在线</string> <string name="connected_count">连接: %1$s 中 %2$s 在线</string>
<string name="list_of_nodes">网络中节点列表</string> <string name="list_of_nodes">网络中节点列表</string>
<string name="update_firmware">更新固件</string> <string name="update_firmware">更新固件</string>
<string name="connected">连接设备</string> <string name="connected">连接设备</string>

View file

@ -52,7 +52,7 @@
<string name="share">Share</string> <string name="share">Share</string>
<string name="disconnected">Disconnected</string> <string name="disconnected">Disconnected</string>
<string name="device_sleeping">Device sleeping</string> <string name="device_sleeping">Device sleeping</string>
<string name="connected_count">Connected: %s of %s online</string> <string name="connected_count">Connected: %1$s of %2$s online</string>
<string name="list_of_nodes">A list of nodes in the network</string> <string name="list_of_nodes">A list of nodes in the network</string>
<string name="update_firmware">Update Firmware</string> <string name="update_firmware">Update Firmware</string>
<string name="connected">Connected to radio</string> <string name="connected">Connected to radio</string>
@ -120,7 +120,10 @@
<string name="why_camera_required">We must be granted access to the camera to read QR codes. No pictures or videos will be saved.</string> <string name="why_camera_required">We must be granted access to the camera to read QR codes. No pictures or videos will be saved.</string>
<string name="modem_config_slow_short">Short Range / Slow</string> <string name="modem_config_slow_short">Short Range / Slow</string>
<string name="modem_config_slow_medium">Medium Range / Slow</string> <string name="modem_config_slow_medium">Medium Range / Slow</string>
<string name="delete_selected_message">Delete selected message?</string> <plurals name="delete_messages">
<item quantity="one">Delete message?</item>
<item quantity="other">Delete %s messages?</item>
</plurals>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="delete_all_messages">Delete All Messages</string> <string name="select_all">Select all</string>
</resources> </resources>

View file

@ -11,7 +11,8 @@
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
<item name="android:itemTextAppearance">@style/menu_item_color</item> <item name="android:itemTextAppearance">@style/menu_item_color</item>
<item name="actionModeStyle">@style/MyActionMode</item>
<item name="windowActionModeOverlay">true</item>
</style> </style>
<style name="AppTheme.Spinner"> <style name="AppTheme.Spinner">
@ -73,6 +74,12 @@
<item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item> <item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item>
</style> </style>
<style name="MyActionMode" parent="Base.Widget.AppCompat.ActionMode">
<item name="background">@color/colorPrimary</item>
<item name="android:textSize">16sp</item>
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
</style>
<style name="Theme.App.Starting" parent="Theme.SplashScreen"> <style name="Theme.App.Starting" parent="Theme.SplashScreen">
// Set the splash screen background, animated icon, and animation duration. // Set the splash screen background, animated icon, and animation duration.
<item name="windowSplashScreenBackground">@color/selectedColor</item> <item name="windowSplashScreenBackground">@color/selectedColor</item>

View file

@ -2,7 +2,7 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.6.10'
ext.coroutines_version = "1.5.2" ext.coroutines_version = "1.6.0"
ext.hilt_version = '2.40.5' ext.hilt_version = '2.40.5'
repositories { repositories {
@ -10,7 +10,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"