Compare commits

...

16 commits
main ... 1.2.60

Author SHA1 Message Date
Andre Kirchhoff
1f3d817e3b
Merge pull request #394 from meshtastic/1.2-release
1.2.60
2022-03-19 17:22:18 -03:00
andrekir
bf8f4f1660 1.2.60 2022-03-19 17:12:24 -03:00
andrekir
6258780106 update mapbox tokens 2022-03-15 01:33:30 -03:00
andrekir
67794f0433 improve firmware update 2022-03-11 00:03:46 -03:00
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
46 changed files with 633 additions and 348 deletions

View file

@ -19,11 +19,12 @@ jobs:
- name: Load secrets
run: |
rm ./app/src/main/res/values/mapbox-token.xml
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOXTOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOX_ACCESS_TOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
mkdir -p ~/.gradle
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOXTOKEN" >>~/.gradle/gradle.properties
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOX_DOWNLOADS_TOKEN" >>~/.gradle/gradle.properties
env:
MAPBOXTOKEN: ${{ secrets.MAPBOXTOKEN }}
MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }}
MAPBOX_DOWNLOADS_TOKEN: ${{ secrets.MAPBOX_DOWNLOADS_TOKEN }}
- name: Mock files for CI
run: |

View file

@ -22,14 +22,15 @@ jobs:
rm ./app/google-services.json
echo $GSERVICES > ./app/google-services.json
rm ./app/src/main/res/values/mapbox-token.xml
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOXTOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOX_ACCESS_TOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
mkdir -p ~/.gradle
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOXTOKEN" >> ~/.gradle/gradle.properties
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOX_DOWNLOADS_TOKEN" >> ~/.gradle/gradle.properties
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
env:
GSERVICES: ${{ secrets.GSERVICES }}
MAPBOXTOKEN: ${{ secrets.MAPBOXTOKEN }}
MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }}
MAPBOX_DOWNLOADS_TOKEN: ${{ secrets.MAPBOX_DOWNLOADS_TOKEN }}
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
@ -47,6 +48,7 @@ jobs:
with:
repository: meshtastic/Meshtastic-device
releases-only: true
prefix: 'v1.2.'
token: ${{ secrets.GITHUB_TOKEN }}
- name: Create version strings

View file

@ -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 20260 // format is Mmmss (where M is 1+the numeric major number
versionName "1.2.60"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// per https://developer.android.com/studio/write/vector-asset-studio
@ -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"

View file

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

View file

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

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.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
}
private val bluetoothViewModel: BluetoothViewModel by viewModels()
val model: UIViewModel by viewModels()
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
@ -187,28 +183,6 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
private val btStateReceiver = BluetoothStateReceiver {
updateBluetoothEnabled()
}
/**
* Don't tell our app we have bluetooth until we have bluetooth _and_ location access
*/
private fun updateBluetoothEnabled() {
var enabled = false // assume failure
if (hasConnectPermission()) {
/// ask the adapter if we have access
bluetoothAdapter?.apply {
enabled = isEnabled
}
} else
errormsg("Still missing needed bluetooth permissions")
debug("Detected our bluetooth access=$enabled")
model.bluetoothEnabled.value = enabled
}
/** Get the minimum permissions our app needs to run correctly
*/
private fun getMinimumPermissions(): List<String> {
@ -381,7 +355,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
updateBluetoothEnabled()
bluetoothViewModel.permissionsUpdated()
}
@ -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 {

View file

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

View file

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

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

View file

@ -74,10 +74,6 @@ class UIViewModel @Inject constructor(
debug("ViewModel created")
}
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(packet)
}
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
@ -229,10 +225,6 @@ class UIViewModel @Inject constructor(
val ownerName = object : MutableLiveData<String>("MrIDE Test") {
}
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
}
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) {
override fun setValue(value: Boolean) {
super.setValue(value)
@ -243,9 +235,6 @@ class UIViewModel @Inject constructor(
}
}
/// If the app was launched because we received a new channel intent, the Url will be here
var requestedChannelUrl: Uri? = null
// clean up all this nasty owner state management FIXME
fun setOwner(s: String? = null) {
@ -283,66 +272,83 @@ class UIViewModel @Inject constructor(
// Capture the current node value while we're still on main thread
val nodes = nodeDB.nodes.value ?: emptyMap()
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
meshPosition?.let { Position(it) }.takeIf {
it?.isValid() == true
}
}
writeToUri(file_uri) { writer ->
// Create a map of nodes keyed by their ID
val nodesById = nodes.values.associateBy { it.num }
val nodesById = nodes.values.associateBy { it.num }.toMutableMap()
val nodePositions = mutableMapOf<Int, MeshProtos.Position?>()
writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
// Packets are ordered by time, we keep most recent position of
// our device in localNodePosition.
var localNodePosition: MeshProtos.Position? = null
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
repository.getAllPacketsInReceiveOrder().first().forEach { packet ->
packet.proto?.let { proto ->
repository.getAllPacketsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
// If we get a NodeInfo packet, use it to update our position data (if valid)
packet.nodeInfo?.let { nodeInfo ->
positionToPos.invoke(nodeInfo.position)?.let { _ ->
nodePositions[nodeInfo.num] = nodeInfo.position
}
}
packet.meshPacket?.let { proto ->
// If the packet contains position data then use it to update, if valid
packet.position?.let { position ->
if (proto.from == myNodeNum) {
localNodePosition = position
} else {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long
val senderPos = packet.position
?.let { p -> Position(p) }
?.takeIf { p -> p.isValid() }
val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation
val rxPos = localNodePosition
?.let { p -> Position(p) }
?.takeIf { p -> p.isValid() }
val rxLat = rxPos?.latitude ?: ""
val rxLong = rxPos?.longitude ?: ""
val rxAlt = rxPos?.altitude ?: ""
val rxSnr = "%f".format(proto.rxSnr)
// Calculate the distance if both positions are valid
val dist = if (senderPos == null || rxPos == null) {
""
} else {
positionToMeter(
localNodePosition!!,
position
).roundToInt().toString()
}
val hopLimit = proto.hopLimit
val payload = when {
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
.replace("\"", "\\\"") + "\""
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
else -> ""
}
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
positionToPos.invoke(position)?.let { _ ->
nodePositions[proto.from] = position
}
}
// Filter out of our results any packet that doesn't report SNR. This
// is primarily ADMIN_APP.
if (proto.rxSnr > 0.0f) {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long
val senderPosition = nodePositions[proto.from]
val senderPos = positionToPos.invoke(senderPosition)
val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation
val rxPosition = nodePositions[myNodeNum]
val rxPos = positionToPos.invoke(rxPosition)
val rxLat = rxPos?.latitude ?: ""
val rxLong = rxPos?.longitude ?: ""
val rxAlt = rxPos?.altitude ?: ""
val rxSnr = "%f".format(proto.rxSnr)
// Calculate the distance if both positions are valid
val dist = if (senderPos == null || rxPos == null) {
""
} else {
positionToMeter(
rxPosition!!, // Use rxPosition but only if rxPos was valid
senderPosition!! // Use senderPosition but only if senderPos was valid
).roundToInt().toString()
}
val hopLimit = proto.hopLimit
val payload = when {
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
.replace("\"", "\\\"") + "\""
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
else -> ""
}
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.geeksville.mesh.service
package com.geeksville.mesh.repository.bluetooth
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
@ -6,29 +6,26 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.geeksville.util.exceptionReporter
import javax.inject.Inject
/**
* A helper class to call onChanged when bluetooth is enabled or disabled
*/
class BluetoothStateReceiver(
private val onChanged: (Boolean) -> Unit
class BluetoothBroadcastReceiver @Inject constructor(
private val bluetoothRepository: BluetoothRepository
) : BroadcastReceiver() {
val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
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 -> onChanged(false)
BluetoothAdapter.STATE_ON -> onChanged(true)
BluetoothAdapter.STATE_OFF -> bluetoothRepository.refreshState()
BluetoothAdapter.STATE_ON -> bluetoothRepository.refreshState()
}
}
}
private val Intent.bluetoothAdapterState: Int
get() = getIntExtra(
BluetoothAdapter.EXTRA_STATE,
-1
)
get() = getIntExtra(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

@ -1309,13 +1309,14 @@ class MeshService : Service(), Logging {
if (asStr != null)
hwModelStr = asStr
}
setFirmwareUpdateFilename(hwModelStr)
val mi = with(myInfo) {
MyNodeInfo(
myNodeNum,
hasGps,
hwModelStr,
firmwareVersion,
firmwareUpdateFilename != null,
firmwareUpdateFilename?.appLoad != null && firmwareUpdateFilename?.spiffs != null,
isBluetoothInterface && SoftwareUpdateService.shouldUpdate(
this@MeshService,
DeviceVersion(firmwareVersion)
@ -1328,9 +1329,7 @@ class MeshService : Service(), Logging {
airUtilTx
)
}
newMyNodeInfo = mi
setFirmwareUpdateFilename(mi)
}
}
@ -1560,7 +1559,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)
@ -1670,12 +1669,12 @@ class MeshService : Service(), Logging {
/***
* Return the filename we will install on the device
*/
private fun setFirmwareUpdateFilename(info: MyNodeInfo) {
private fun setFirmwareUpdateFilename(model: String?) {
firmwareUpdateFilename = try {
if (info.firmwareVersion != null && info.model != null)
if (model != null)
SoftwareUpdateService.getUpdateFilename(
this,
info.model
model
)
else
null
@ -1782,10 +1781,12 @@ class MeshService : Service(), Logging {
this@MeshService.setOwner(myId, longName, shortName)
}
override fun delete(position: Int) {
if (position >= 0) {
recentDataPackets.removeAt(position)
}
override fun deleteMessage(packetId: Int) {
val packet = recentDataPackets.find {it.id == packetId}
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() {

View file

@ -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.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() {
unregisterReceiver(bluetoothStateReceiver)
stopInterface()
serviceScope.cancel("Destroying RadioInterface")
runningService = null
lifecycleDispatcher.onServicePreSuperOnDestroy()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
lifecycleDispatcher.onServicePreSuperOnBind()
return binder
}

View file

@ -243,7 +243,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
false // If we fail parsing our update info
}
/** Return a Pair of apploadfilename, spiffs filename this device needs to use as an update (or null if no update needed)
/** Return a Pair of appload filename, spiffs filename this device needs to use as an update (or null if no update needed)
*/
fun getUpdateFilename(
context: Context,
@ -290,9 +290,15 @@ class SoftwareUpdateService : JobIntentService(), Logging {
* you can use it for the software update.
*/
fun doUpdate(context: Context, sync: SafeBluetooth, assets: UpdateFilenames) {
// calculate total firmware size (spiffs + appLoad)
var totalFirmwareSize = 0
if (assets.appLoad != null && assets.spiffs != null) {
totalFirmwareSize += context.assets.open(assets.appLoad).available()
totalFirmwareSize += context.assets.open(assets.spiffs).available()
}
// we must attempt spiffs first, because if we update the appload the device will reboot afterwards
try {
assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) }
assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS, totalFirmwareSize) }
} catch (_: BLECharacteristicNotFoundException) {
// If we can't update spiffs (because not supported by target), do not fail
errormsg("Ignoring failure to update spiffs on old appload")
@ -301,7 +307,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
errormsg("Device rejected invalid spiffs partition")
}
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) }
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD, totalFirmwareSize) }
sendProgress(context, ProgressSuccess, true)
}
@ -317,7 +323,8 @@ class SoftwareUpdateService : JobIntentService(), Logging {
context: Context,
sync: SafeBluetooth,
assetName: String,
flashRegion: Int = FLASH_REGION_APPLOAD
flashRegion: Int = FLASH_REGION_APPLOAD,
totalFirmwareSize: Int = 0
) {
val isAppload = flashRegion == FLASH_REGION_APPLOAD
@ -378,13 +385,15 @@ class SoftwareUpdateService : JobIntentService(), Logging {
// Send all the blocks
var oldProgress = -1 // used to limit # of log spam
while (firmwareNumSent < firmwareSize) {
// If we are doing the spiffs partition, we limit progress to a max of 50%, so that the user doesn't think we are done
// yet
val maxProgress = if (flashRegion != FLASH_REGION_APPLOAD)
50 else 100
// If we are doing the spiffs partition, we limit progress to a max of maxProgress
// when updating the appload partition, progress from (100 - maxProgress) to 100%
// maxProgress = spiffs% = 100% - appLoad%; (int * 10 + 5) / 10 used for rounding
val maxProgress = ((firmwareSize * 1000 / totalFirmwareSize) + 5) / 10
val minProgress = if (flashRegion != FLASH_REGION_APPLOAD)
0 else (100 - maxProgress)
sendProgress(
context,
firmwareNumSent * maxProgress / firmwareSize,
minProgress + firmwareNumSent * maxProgress / firmwareSize,
isAppload
)
if (progress != oldProgress) {

View file

@ -1,12 +1,10 @@
package com.geeksville.mesh.ui
import android.app.AlertDialog
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
@ -14,11 +12,11 @@ import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
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.service.MeshService
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat
import java.util.*
@ -37,7 +36,6 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
if (actionId == receivedActionId) {
func()
}
true
}
}
@ -45,6 +43,7 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
@AndroidEntryPoint
class MessagesFragment : ScreenFragment("Messages"), Logging {
private var actionMode: ActionMode? = null
private var _binding: MessagesFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
@ -53,7 +52,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
private val model: UIViewModel by activityViewModels()
// Allows textMultiline with IME_ACTION_SEND
fun EditText.onActionSend(func: () -> Unit) {
private fun EditText.onActionSend(func: () -> Unit) {
setImeOptions(EditorInfo.IME_ACTION_SEND)
setRawInputType(InputType.TYPE_CLASS_TEXT)
setOnEditorActionListener { _, actionId, _ ->
@ -61,7 +60,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
if (actionId == EditorInfo.IME_ACTION_SEND) {
func()
}
true
}
}
@ -73,22 +71,21 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
private fun getShortDateTime(time: Date): String {
// return time if within 24 hours, otherwise date/time
val one_day = 60 * 60 * 24 * 1000
if (System.currentTimeMillis() - time.time > one_day) {
return dateTimeFormat.format(time)
} else return timeFormat.format(time)
val oneDayMsec = 60 * 60 * 24 * 1000L
return if (System.currentTimeMillis() - time.time > oneDayMsec) {
dateTimeFormat.format(time)
} else timeFormat.format(time)
}
// 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
class ViewHolder(itemView: AdapterMessageLayoutBinding) :
RecyclerView.ViewHolder(itemView.root) {
val card: CardView = itemView.Card
val username: Chip = itemView.username
val messageText: TextView = itemView.messageText
val messageTime: TextView = itemView.messageTime
val messageStatusIcon: ImageView = itemView.messageStatusIcon
val card: CardView = itemView.Card
}
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
@ -119,8 +116,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
// Inflate the custom layout
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
@ -128,6 +123,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
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.
*
@ -159,7 +157,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val msg = messages[position]
val nodes = model.nodeDB.nodes.value!!
val node = nodes.get(msg.from)
// Determine if this is my message (originated on this device).
// val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
val isMe = msg.from == "^local"
@ -167,48 +165,19 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// Set cardview offset and color.
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
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) {
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMyMsg
)
)
holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg))
}
} else {
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMsg
)
)
holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg))
}
}
// Hide the username chip for my messages
@ -217,11 +186,14 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
} else {
holder.username.visibility = View.VISIBLE
// If we can't find the sender, just use the ID
val node = nodes[msg.from]
val user = node?.user
holder.username.text = user?.shortName ?: msg.from
}
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
} else {
holder.messageText.text = msg.text
@ -243,9 +215,114 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
} else
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
}
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 var messages = arrayOf<DataPacket>()
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
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
@ -258,10 +335,15 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
}
}
override fun onPause() {
actionMode?.finish()
super.onPause()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
return binding.root
}
@ -269,7 +351,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.sendButton.setOnClickListener {
debug("sendButton click")
debug("User clicked sendButton")
val str = binding.messageInputText.text.toString().trim()
if (str.isNotEmpty())
@ -295,34 +377,20 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
layoutManager.stackFromEnd = true // We want the last rows to always be shown
binding.messageListView.layoutManager = layoutManager
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
model.messagesState.messages.observe(viewLifecycleOwner) {
debug("New messages received: ${it.size}")
messagesAdapter.onMessagesChanged(it)
})
}
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
fun updateTextEnabled() {
binding.textInputLayout.isEnabled =
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED
model.isConnected.observe(viewLifecycleOwner) { connectionState ->
// If we don't know our node ID and we are offline don't let user try to send
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
// && 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

@ -24,6 +24,7 @@ import android.widget.*
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
@ -33,6 +34,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 +449,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
@ -472,6 +475,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
model.meshService?.let { service ->
debug("User started firmware update")
GeeksvilleApplication.analytics.track(
"firmware_update",
DataPair("content_type", "start")
)
binding.updateFirmwareButton.isEnabled = false // Disable until things complete
binding.updateProgressBar.visibility = View.VISIBLE
binding.updateProgressBar.progress = 0 // start from scratch
@ -513,6 +520,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
} else
when (progress) {
ProgressSuccess -> {
GeeksvilleApplication.analytics.track(
"firmware_update",
DataPair("content_type", "success")
)
binding.scanStatusText.setText(R.string.update_successful)
binding.updateProgressBar.visibility = View.GONE
}
@ -521,6 +532,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.updateProgressBar.visibility = View.GONE
}
else -> {
GeeksvilleApplication.analytics.track(
"firmware_update",
DataPair("content_type", "failure")
)
binding.scanStatusText.setText(R.string.update_failed)
binding.updateProgressBar.visibility = View.VISIBLE
}
@ -624,7 +639,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 +828,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)
}

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

View file

@ -45,7 +45,7 @@
<string name="share">Compartir</string>
<string name="disconnected">Desconectado</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="update_firmware">Actualizar el firmware</string>
<string name="connected">Conectado a la radio</string>

View file

@ -49,7 +49,7 @@
<string name="share">Partager</string>
<string name="disconnected">Déconnecté</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="update_firmware">Mise à jour du Firmware</string>
<string name="connected">Connecté à une radio</string>

View file

@ -46,7 +46,7 @@
<string name="share">Pataje</string>
<string name="disconnected">Dekonekte</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="update_firmware">Mete ajou mikrolojisyèl</string>
<string name="connected">Konekte ak radyo</string>

View file

@ -47,7 +47,7 @@
<string name="share">Megosztás</string>
<string name="disconnected">Szétkapcsolva</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="update_firmware">Firmware frissítés</string>
<string name="connected">Kapcsolódva a rádióhoz</string>

View file

@ -48,7 +48,7 @@ mapboxの有償プランまたは代替地図プロバイダを検討さ
<string name="share">シェア</string>
<string name="disconnected">切断</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="update_firmware">ファームウェアアップデート</string>
<string name="connected_to">Meshtasticデバイスに接続しました。(%s)</string>

View file

@ -46,7 +46,7 @@
<string name="share">공유</string>
<string name="disconnected">연결 해제</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="update_firmware">펌웨어 업데이트</string>
<string name="connected">라디오로 연결됨</string>

View file

@ -48,7 +48,7 @@
<string name="share">Deel</string>
<string name="disconnected">Niet verbonden</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="update_firmware">Programma Updaten</string>
<string name="connected">Verbonden met een radio</string>

View file

@ -48,7 +48,7 @@
<string name="share">Del</string>
<string name="disconnected">Frakoblet</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="update_firmware">Oppdater Firmware</string>
<string name="connected">Tilkoblet radio</string>

View file

@ -53,7 +53,7 @@
<string name="share">Udostępnij</string>
<string name="disconnected">Rozłączone</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="update_firmware">Aktualizuj oprogramowanie.</string>
<string name="connected">Połączony z urządzeniem</string>

View file

@ -1,5 +1,5 @@
<?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="channel_name">Nome do canal</string>
<string name="channel_options">Opções do canal</string>
@ -48,7 +48,7 @@
<string name="share">Compartilhar</string>
<string name="disconnected">Desconectado</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="update_firmware">Atualizar Firmware</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="provide_location_to_mesh">Fornecer localização para mesh</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_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>

View file

@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="action_settings">Configurações</string>
<string name="channel_name">Nome do Canal</string>
<string name="channel_options">Opções do Canal</string>
@ -47,7 +47,7 @@
<string name="share">Partilha</string>
<string name="disconnected">Desconectado</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="update_firmware">Atualizar Firmware</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="allow_will_show">Permitir (exibe diálogo)</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="modem_config_slow_short">Curto 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>

View file

@ -48,7 +48,7 @@
<string name="share">Distribuie</string>
<string name="disconnected">Deconectat</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="update_firmware">Updateaza firmware-ul</string>
<string name="connected">Connectat la dispozitiv</string>

View file

@ -48,7 +48,7 @@
<string name="share">Zdieľať</string>
<string name="disconnected">Odpojené</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="update_firmware">Aktualizácia firmvéru</string>
<string name="connected">Pripojené k vysielaču</string>

View file

@ -46,7 +46,7 @@
<string name="share">Deliti</string>
<string name="disconnected">Prekinjeno</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="update_firmware">Posodobite vdelano programsko opremo</string>
<string name="connected">Povezana z radiem</string>

View file

@ -48,7 +48,7 @@
<string name="share">Paylaş</string>
<string name="disconnected">Bağlantı sonlandı</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="update_firmware">Yazılım güncelle</string>
<string name="connected">Radyoya bağlandı</string>

View file

@ -48,7 +48,7 @@
<string name="share">分享</string>
<string name="disconnected">断开连接</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="update_firmware">更新固件</string>
<string name="connected">连接设备</string>

View file

@ -52,7 +52,7 @@
<string name="share">Share</string>
<string name="disconnected">Disconnected</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="update_firmware">Update Firmware</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="modem_config_slow_short">Short 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_all_messages">Delete All Messages</string>
<string name="select_all">Select all</string>
</resources>

View file

@ -11,7 +11,8 @@
<item name="windowNoTitle">true</item>
<item name="android:itemTextAppearance">@style/menu_item_color</item>
<item name="actionModeStyle">@style/MyActionMode</item>
<item name="windowActionModeOverlay">true</item>
</style>
<style name="AppTheme.Spinner">
@ -73,6 +74,12 @@
<item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item>
</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">
// Set the splash screen background, animated icon, and animation duration.
<item name="windowSplashScreenBackground">@color/selectedColor</item>

View file

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