mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3d817e3b | ||
|
|
bf8f4f1660 | ||
|
|
6258780106 | ||
|
|
67794f0433 | ||
|
|
4092fc5c7f | ||
|
|
91b2767634 | ||
|
|
52f7a862b3 | ||
|
|
ede48be4f3 | ||
|
|
598ec54cf3 | ||
|
|
49188adc36 | ||
|
|
c0fe9213f1 | ||
|
|
1294eee8e3 | ||
|
|
5659725b96 | ||
|
|
6ad2b1814f | ||
|
|
01f8154189 | ||
|
|
7395cc5583 |
46 changed files with 633 additions and 348 deletions
7
.github/workflows/android.yml
vendored
7
.github/workflows/android.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -66,8 +66,7 @@ interface IMeshService {
|
|||
*/
|
||||
void send(inout DataPacket packet);
|
||||
|
||||
|
||||
void delete(int position);
|
||||
void deleteMessage(int packetId);
|
||||
|
||||
void deleteAllMessages();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.geeksville.mesh
|
|||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,4 +18,14 @@ object ApplicationModule {
|
|||
fun provideSharedPreferences(application: Application): SharedPreferences {
|
||||
return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideProcessLifecycleOwner(): LifecycleOwner {
|
||||
return ProcessLifecycleOwner.get()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideProcessLifecycle(processLifecycleOwner: LifecycleOwner): Lifecycle {
|
||||
return processLifecycleOwner.lifecycle
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.geeksville.mesh
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Wrapper around `Dispatchers` to allow for easier testing when using dispatchers
|
||||
* in injected classes.
|
||||
*/
|
||||
class CoroutineDispatchers @Inject constructor() {
|
||||
val main = Dispatchers.Main
|
||||
val mainImmediate = Dispatchers.Main.immediate
|
||||
val default = Dispatchers.Default
|
||||
val io = Dispatchers.IO
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.*
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
|
|
@ -40,6 +39,7 @@ import com.geeksville.android.ServiceClient
|
|||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.ChannelSet
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
|
|
@ -134,11 +134,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
// Used to schedule a coroutine in the GUI thread
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
bluetoothManager.adapter
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
|
|||
packetDao.getAllPacket(MAX_ITEMS)
|
||||
}
|
||||
|
||||
suspend fun getAllPacketsInReceiveOrder(): Flow<List<Packet>> = withContext(Dispatchers.IO) {
|
||||
packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
|
||||
suspend fun getAllPacketsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<Packet>> = withContext(Dispatchers.IO) {
|
||||
packetDao.getAllPacketsInReceiveOrder(maxItems)
|
||||
}
|
||||
|
||||
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ data class Packet(@PrimaryKey val uuid: String,
|
|||
@ColumnInfo(name = "message") val raw_message: String
|
||||
) {
|
||||
|
||||
val proto: MeshProtos.MeshPacket?
|
||||
val meshPacket: MeshProtos.MeshPacket?
|
||||
get() {
|
||||
if (message_type == "packet") {
|
||||
val builder = MeshProtos.MeshPacket.newBuilder()
|
||||
|
|
@ -28,13 +28,27 @@ data class Packet(@PrimaryKey val uuid: String,
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val nodeInfo: MeshProtos.NodeInfo?
|
||||
get() {
|
||||
if (message_type == "NodeInfo") {
|
||||
val builder = MeshProtos.NodeInfo.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val position: MeshProtos.Position?
|
||||
get() {
|
||||
return proto?.run {
|
||||
return meshPacket?.run {
|
||||
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
|
||||
return MeshProtos.Position.parseFrom(decoded.payload)
|
||||
}
|
||||
return null
|
||||
}
|
||||
} ?: nodeInfo?.position
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.geeksville.mesh.repository.bluetooth
|
||||
|
||||
import android.app.Application
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BluetoothRepositoryModule {
|
||||
companion object {
|
||||
@Provides
|
||||
fun provideBluetoothManager(application: Application): BluetoothManager? {
|
||||
return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideBluetoothAdapter(service: BluetoothManager?): BluetoothAdapter? {
|
||||
return service?.adapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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
|
||||
)
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}) */
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
15
app/src/main/res/drawable/ic_twotone_delete_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_delete_24.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_twotone_select_all_24.xml
Normal file
10
app/src/main/res/drawable/ic_twotone_select_all_24.xml
Normal 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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
14
app/src/main/res/menu/menu_messages.xml
Normal file
14
app/src/main/res/menu/menu_messages.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue