kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
commit
b44a104be2
|
@ -30,15 +30,15 @@ android {
|
||||||
keyPassword "$meshtasticKeyPassword"
|
keyPassword "$meshtasticKeyPassword"
|
||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
// leave undefined to use version plugin wants
|
// leave undefined to use version plugin wants
|
||||||
// buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI
|
// buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.geeksville.mesh"
|
applicationId "com.geeksville.mesh"
|
||||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode 20211 // format is Mmmss (where M is 1+the numeric major number
|
versionCode 20213 // format is Mmmss (where M is 1+the numeric major number
|
||||||
versionName "1.2.11"
|
versionName "1.2.13"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||||
|
@ -151,7 +151,7 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
// For now I'm not using javalite, because I want JSON printing
|
// For now I'm not using javalite, because I want JSON printing
|
||||||
implementation ('com.google.protobuf:protobuf-java:3.15.5')
|
implementation ('com.google.protobuf:protobuf-java:3.15.6')
|
||||||
|
|
||||||
// For UART access
|
// For UART access
|
||||||
// implementation 'com.google.android.things:androidthings:1.0'
|
// implementation 'com.google.android.things:androidthings:1.0'
|
||||||
|
@ -170,7 +170,7 @@ dependencies {
|
||||||
implementation 'com.google.android.gms:play-services-auth:19.0.0'
|
implementation 'com.google.android.gms:play-services-auth:19.0.0'
|
||||||
|
|
||||||
// Add the Firebase SDK for Crashlytics.
|
// Add the Firebase SDK for Crashlytics.
|
||||||
implementation 'com.google.firebase:firebase-crashlytics:17.3.1'
|
implementation 'com.google.firebase:firebase-crashlytics:17.4.0'
|
||||||
|
|
||||||
// alas implementation bug deep in the bowels when I tried it for my SyncBluetoothDevice class
|
// alas implementation bug deep in the bowels when I tried it for my SyncBluetoothDevice class
|
||||||
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
|
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
|
||||||
|
|
|
@ -113,4 +113,7 @@ interface IMeshService {
|
||||||
Return a number 0-100 for progress. -1 for completed and success, -2 for failure
|
Return a number 0-100 for progress. -1 for completed and success, -2 for failure
|
||||||
*/
|
*/
|
||||||
int getUpdateStatus();
|
int getUpdateStatus();
|
||||||
|
|
||||||
|
int getRegion();
|
||||||
|
void setRegion(int regionCode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -663,30 +663,32 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
|
|
||||||
debug("Getting latest radioconfig from service")
|
debug("Getting latest radioconfig from service")
|
||||||
try {
|
try {
|
||||||
val info = service.myNodeInfo
|
val info: MyNodeInfo? = service.myNodeInfo // this can be null
|
||||||
model.myNodeInfo.value = info
|
model.myNodeInfo.value = info
|
||||||
|
|
||||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
if (info != null) {
|
||||||
if (isOld)
|
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||||
showAlert(R.string.app_too_old, R.string.must_update)
|
if (isOld)
|
||||||
else {
|
showAlert(R.string.app_too_old, R.string.must_update)
|
||||||
|
|
||||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
|
||||||
if (curVer < MeshService.minFirmwareVersion)
|
|
||||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
|
||||||
else {
|
else {
|
||||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
|
||||||
|
|
||||||
model.radioConfig.value =
|
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
if (curVer < MeshService.minFirmwareVersion)
|
||||||
|
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||||
|
else {
|
||||||
|
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||||
|
|
||||||
model.channels.value =
|
model.radioConfig.value =
|
||||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||||
|
|
||||||
updateNodesFromDevice()
|
model.channels.value =
|
||||||
|
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||||
|
|
||||||
// we have a connection to our device now, do the channel change
|
updateNodesFromDevice()
|
||||||
perhapsChangeChannel()
|
|
||||||
|
// we have a connection to our device now, do the channel change
|
||||||
|
perhapsChangeChannel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: RemoteException) {
|
} catch (ex: RemoteException) {
|
||||||
|
@ -972,12 +974,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bindMeshService()
|
bindMeshService()
|
||||||
}
|
} catch (ex: BindFailedException) {
|
||||||
catch(ex: BindFailedException) {
|
|
||||||
// App is probably shutting down, ignore
|
// App is probably shutting down, ignore
|
||||||
errormsg("Bind of MeshService failed")
|
errormsg("Bind of MeshService failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
|
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
|
||||||
if (!bonded && usbDevice == null) // we will handle USB later
|
if (!bonded && usbDevice == null) // we will handle USB later
|
||||||
showSettingsPage()
|
showSettingsPage()
|
||||||
|
|
|
@ -17,10 +17,15 @@ data class Channel(
|
||||||
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf
|
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val cleartextPSK = ByteString.EMPTY
|
||||||
|
private val defaultPSK = byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK
|
||||||
|
|
||||||
// TH=he unsecured channel that devices ship with
|
// TH=he unsecured channel that devices ship with
|
||||||
val defaultChannel = Channel(
|
val defaultChannel = Channel(
|
||||||
ChannelProtos.ChannelSettings.newBuilder()
|
ChannelProtos.ChannelSettings.newBuilder()
|
||||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
|
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096)
|
||||||
|
.setPsk(ByteString.copyFrom(defaultPSK))
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +55,7 @@ data class Channel(
|
||||||
val pskIndex = settings.psk.byteAt(0).toInt()
|
val pskIndex = settings.psk.byteAt(0).toInt()
|
||||||
|
|
||||||
if (pskIndex == 0)
|
if (pskIndex == 0)
|
||||||
ByteString.EMPTY // Treat as an empty PSK (no encryption)
|
cleartextPSK
|
||||||
else {
|
else {
|
||||||
// Treat an index of 1 as the old channelDefaultKey and work up from there
|
// Treat an index of 1 as the old channelDefaultKey and work up from there
|
||||||
val bytes = channelDefaultKey.clone()
|
val bytes = channelDefaultKey.clone()
|
||||||
|
|
|
@ -134,15 +134,10 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var region: RadioConfigProtos.RegionCode?
|
var region: RadioConfigProtos.RegionCode
|
||||||
get() = radioConfig.value?.preferences?.region
|
get() = meshService?.region?.let { RadioConfigProtos.RegionCode.forNumber(it) } ?: RadioConfigProtos.RegionCode.Unset
|
||||||
set(value) {
|
set(value) {
|
||||||
val config = radioConfig.value
|
meshService?.region = value.number
|
||||||
if (value != null && config != null) {
|
|
||||||
val builder = config.toBuilder()
|
|
||||||
builder.preferencesBuilder.region = value
|
|
||||||
setRadioConfig(builder.build())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// hardware info about our local device (can be null)
|
/// hardware info about our local device (can be null)
|
||||||
|
|
|
@ -5,4 +5,7 @@ import java.util.*
|
||||||
|
|
||||||
open class BLEException(msg: String) : IOException(msg)
|
open class BLEException(msg: String) : IOException(msg)
|
||||||
|
|
||||||
open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
|
open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
|
||||||
|
|
||||||
|
/// Our interface is being shut down
|
||||||
|
open class BLEConnectionClosing() : BLEException("Connection closing ")
|
|
@ -313,7 +313,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||||
var fromNumChanged = false
|
var fromNumChanged = false
|
||||||
|
|
||||||
private fun startWatchingFromNum() {
|
private fun startWatchingFromNum() {
|
||||||
safe!!.setNotify(fromNum, true) {
|
safe?.setNotify(fromNum, true) {
|
||||||
// We might get multiple notifies before we get around to reading from the radio - so just set one flag
|
// We might get multiple notifies before we get around to reading from the radio - so just set one flag
|
||||||
fromNumChanged = true
|
fromNumChanged = true
|
||||||
debug("fromNum changed")
|
debug("fromNum changed")
|
||||||
|
@ -469,7 +469,12 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||||
safe =
|
safe =
|
||||||
null // We do this first, because if we throw we still want to mark that we no longer have a valid connection
|
null // We do this first, because if we throw we still want to mark that we no longer have a valid connection
|
||||||
|
|
||||||
s?.close()
|
try {
|
||||||
|
s?.close()
|
||||||
|
}
|
||||||
|
catch(_: BLEConnectionClosing) {
|
||||||
|
warn("Ignoring BLE errors while closing")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debug("Radio was not connected, skipping disable")
|
debug("Radio was not connected, skipping disable")
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ class MeshService : Service(), Logging {
|
||||||
/* @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED")
|
/* @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED")
|
||||||
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" */
|
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" */
|
||||||
|
|
||||||
fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
private fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||||
|
|
||||||
/// generate a RECEIVED action filter string that includes either the portnumber as an int, or preferably a symbolic name from portnums.proto
|
/// generate a RECEIVED action filter string that includes either the portnumber as an int, or preferably a symbolic name from portnums.proto
|
||||||
fun actionReceived(portNum: Int): String {
|
fun actionReceived(portNum: Int): String {
|
||||||
|
@ -549,7 +549,7 @@ class MeshService : Service(), Logging {
|
||||||
setChannel(it)
|
setChannel(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
channels = asChannels.toTypedArray()
|
channels = fixupChannelList(asChannels).toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
|
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
|
||||||
|
@ -946,28 +946,22 @@ class MeshService : Service(), Logging {
|
||||||
/// If we just changed our nodedb, we might want to do somethings
|
/// If we just changed our nodedb, we might want to do somethings
|
||||||
private fun onNodeDBChanged() {
|
private fun onNodeDBChanged() {
|
||||||
maybeUpdateServiceStatusNotification()
|
maybeUpdateServiceStatusNotification()
|
||||||
|
|
||||||
serviceScope.handledLaunch(Dispatchers.Main) {
|
|
||||||
setupLocationRequest()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var locationRequestInterval: Long = 0;
|
|
||||||
private fun setupLocationRequest() {
|
private fun setupLocationRequest() {
|
||||||
val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
|
var desiredInterval = 0L
|
||||||
0L // no requests when device has GPS
|
|
||||||
} else if (numOnlineNodes < 2) {
|
if (myNodeInfo?.hasGPS == true)
|
||||||
5 * 60 * 1000L // send infrequently, device needs these requests to set its clock
|
desiredInterval =
|
||||||
|
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
|
||||||
|
|
||||||
|
stopLocationRequests()
|
||||||
|
if (desiredInterval != 0L) {
|
||||||
|
debug("desired GPS assistance interval $desiredInterval")
|
||||||
|
startLocationRequests(desiredInterval)
|
||||||
} else {
|
} else {
|
||||||
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
|
debug("No GPS assistance desired, but sending UTC time to mesh")
|
||||||
}
|
sendPositionScoped()
|
||||||
|
|
||||||
debug("desired location request $desiredInterval, current $locationRequestInterval")
|
|
||||||
|
|
||||||
if (desiredInterval != locationRequestInterval) {
|
|
||||||
if (locationRequestInterval > 0) stopLocationRequests()
|
|
||||||
if (desiredInterval > 0) startLocationRequests(desiredInterval)
|
|
||||||
locationRequestInterval = desiredInterval
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1315,13 +1309,44 @@ class MeshService : Service(), Logging {
|
||||||
radioConfig = null
|
radioConfig = null
|
||||||
|
|
||||||
// prefill the channel array with null channels
|
// prefill the channel array with null channels
|
||||||
channels = Array(myInfo.maxChannels) {
|
channels = fixupChannelList(listOf<ChannelProtos.Channel>()).toTypedArray()
|
||||||
val b = ChannelProtos.Channel.newBuilder()
|
|
||||||
b.index = it
|
|
||||||
b.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long
|
||||||
|
private fun fixupChannelList(lIn: List<ChannelProtos.Channel>): List<ChannelProtos.Channel> {
|
||||||
|
val mi = myNodeInfo
|
||||||
|
var l = lIn
|
||||||
|
if (mi != null)
|
||||||
|
while (l.size < mi.maxChannels) {
|
||||||
|
val b = ChannelProtos.Channel.newBuilder()
|
||||||
|
b.index = l.size
|
||||||
|
l += b.build()
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setRegionOnDevice() {
|
||||||
|
val curConfigRegion =
|
||||||
|
radioConfig?.preferences?.region ?: RadioConfigProtos.RegionCode.Unset
|
||||||
|
|
||||||
|
if (curConfigRegion.number != curRegionValue && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE)
|
||||||
|
if (deviceVersion >= minFirmwareVersion) {
|
||||||
|
info("Telling device to upgrade region")
|
||||||
|
|
||||||
|
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||||
|
radioConfig?.let { currentConfig ->
|
||||||
|
val newConfig = currentConfig.toBuilder()
|
||||||
|
|
||||||
|
val newPrefs = currentConfig.preferences.toBuilder()
|
||||||
|
newPrefs.regionValue = curRegionValue
|
||||||
|
newConfig.preferences = newPrefs.build()
|
||||||
|
|
||||||
|
sendRadioConfig(newConfig.build())
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
warn("Device is too old to understand region changes")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If we are updating nodes we might need to use old (fixed by firmware build)
|
* If we are updating nodes we might need to use old (fixed by firmware build)
|
||||||
|
@ -1356,24 +1381,7 @@ class MeshService : Service(), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in)
|
// If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in)
|
||||||
if (curConfigRegion == RadioConfigProtos.RegionCode.Unset && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE) {
|
setRegionOnDevice()
|
||||||
if (deviceVersion >= minFirmwareVersion) {
|
|
||||||
info("Telling device to upgrade region")
|
|
||||||
|
|
||||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
|
||||||
radioConfig?.let { currentConfig ->
|
|
||||||
val newConfig = currentConfig.toBuilder()
|
|
||||||
|
|
||||||
val newPrefs = currentConfig.preferences.toBuilder()
|
|
||||||
newPrefs.regionValue = curRegionValue
|
|
||||||
newConfig.preferences = newPrefs.build()
|
|
||||||
|
|
||||||
sendRadioConfig(newConfig.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
warn("Device is too old to understand region changes")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1388,6 +1396,8 @@ class MeshService : Service(), Logging {
|
||||||
reportConnection()
|
reportConnection()
|
||||||
|
|
||||||
updateRegion()
|
updateRegion()
|
||||||
|
|
||||||
|
setupLocationRequest() // start sending location packets if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleConfigComplete(configCompleteId: Int) {
|
private fun handleConfigComplete(configCompleteId: Int) {
|
||||||
|
@ -1466,13 +1476,13 @@ class MeshService : Service(), Logging {
|
||||||
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
|
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
|
||||||
*/
|
*/
|
||||||
private fun sendPosition(
|
private fun sendPosition(
|
||||||
lat: Double,
|
lat: Double = 0.0,
|
||||||
lon: Double,
|
lon: Double = 0.0,
|
||||||
alt: Int,
|
alt: Int = 0,
|
||||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||||
wantResponse: Boolean = false
|
wantResponse: Boolean = false
|
||||||
) {
|
) {
|
||||||
debug("Sending our position to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
debug("Sending our position/time to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||||
|
|
||||||
val position = MeshProtos.Position.newBuilder().also {
|
val position = MeshProtos.Position.newBuilder().also {
|
||||||
it.longitudeI = Position.degI(lon)
|
it.longitudeI = Position.degI(lon)
|
||||||
|
@ -1499,15 +1509,15 @@ class MeshService : Service(), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendPositionScoped(
|
private fun sendPositionScoped(
|
||||||
lat: Double,
|
lat: Double = 0.0,
|
||||||
lon: Double,
|
lon: Double = 0.0,
|
||||||
alt: Int,
|
alt: Int = 0,
|
||||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||||
wantResponse: Boolean = false
|
wantResponse: Boolean = false
|
||||||
) = serviceScope.handledLaunch {
|
) = serviceScope.handledLaunch {
|
||||||
try {
|
try {
|
||||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||||
} catch (ex: RadioNotConnectedException) {
|
} catch (ex: BLEException) {
|
||||||
warn("Ignoring disconnected radio during gps location update")
|
warn("Ignoring disconnected radio during gps location update")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1627,8 +1637,10 @@ class MeshService : Service(), Logging {
|
||||||
} else {
|
} else {
|
||||||
debug("Creating firmware update coroutine")
|
debug("Creating firmware update coroutine")
|
||||||
updateJob = serviceScope.handledLaunch {
|
updateJob = serviceScope.handledLaunch {
|
||||||
debug("Starting firmware update coroutine")
|
exceptionReporter {
|
||||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
debug("Starting firmware update coroutine")
|
||||||
|
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1660,7 +1672,7 @@ class MeshService : Service(), Logging {
|
||||||
offlineSentPackets.add(p)
|
offlineSentPackets.add(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
val binder = object : IMeshService.Stub() {
|
private val binder = object : IMeshService.Stub() {
|
||||||
|
|
||||||
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
||||||
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
||||||
|
@ -1684,6 +1696,12 @@ class MeshService : Service(), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUpdateStatus(): Int = SoftwareUpdateService.progress
|
override fun getUpdateStatus(): Int = SoftwareUpdateService.progress
|
||||||
|
override fun getRegion(): Int = curRegionValue
|
||||||
|
|
||||||
|
override fun setRegion(regionCode: Int) = toRemoteExceptions {
|
||||||
|
curRegionValue = regionCode
|
||||||
|
setRegionOnDevice()
|
||||||
|
}
|
||||||
|
|
||||||
override fun startFirmwareUpdate() = toRemoteExceptions {
|
override fun startFirmwareUpdate() = toRemoteExceptions {
|
||||||
doFirmwareUpdate()
|
doFirmwareUpdate()
|
||||||
|
|
|
@ -325,7 +325,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||||
if (newWork.timeoutMillis != 0L) {
|
if (newWork.timeoutMillis != 0L) {
|
||||||
|
|
||||||
activeTimeout = serviceScope.launch {
|
activeTimeout = serviceScope.launch {
|
||||||
debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
// debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||||
delay(newWork.timeoutMillis)
|
delay(newWork.timeoutMillis)
|
||||||
errormsg("Failsafe BLE timer expired!")
|
errormsg("Failsafe BLE timer expired!")
|
||||||
completeWork(
|
completeWork(
|
||||||
|
@ -415,7 +415,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||||
if (work == null)
|
if (work == null)
|
||||||
warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
|
warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
|
||||||
else {
|
else {
|
||||||
debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
// debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||||
if (status != 0)
|
if (status != 0)
|
||||||
work.completion.resumeWithException(
|
work.completion.resumeWithException(
|
||||||
BLEStatusException(
|
BLEStatusException(
|
||||||
|
@ -773,7 +773,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||||
|
|
||||||
closeGatt()
|
closeGatt()
|
||||||
|
|
||||||
failAllWork(BLEException("Connection closing"))
|
failAllWork(BLEConnectionClosing())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -374,13 +374,17 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||||
throw DeviceRejectedException()
|
throw DeviceRejectedException()
|
||||||
|
|
||||||
// Send all the blocks
|
// Send all the blocks
|
||||||
|
var oldProgress = -1 // used to limit # of log spam
|
||||||
while (firmwareNumSent < firmwareSize) {
|
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
|
// 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
|
// yet
|
||||||
val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD)
|
val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD)
|
||||||
50 else 100
|
50 else 100
|
||||||
sendProgress(context, firmwareNumSent * maxProgress / firmwareSize, isAppload)
|
sendProgress(context, firmwareNumSent * maxProgress / firmwareSize, isAppload)
|
||||||
debug("sending block ${progress}%")
|
if(progress != oldProgress) {
|
||||||
|
debug("sending block ${progress}%")
|
||||||
|
oldProgress = progress;
|
||||||
|
}
|
||||||
var blockSize = 512 - 3 // Max size MTU excluding framing
|
var blockSize = 512 - 3 // Max size MTU excluding framing
|
||||||
|
|
||||||
if (blockSize > firmwareStream.available())
|
if (blockSize > firmwareStream.available())
|
||||||
|
|
|
@ -18,7 +18,6 @@ import com.geeksville.android.Logging
|
||||||
import com.geeksville.android.hideKeyboard
|
import com.geeksville.android.hideKeyboard
|
||||||
import com.geeksville.mesh.AppOnlyProtos
|
import com.geeksville.mesh.AppOnlyProtos
|
||||||
import com.geeksville.mesh.ChannelProtos
|
import com.geeksville.mesh.ChannelProtos
|
||||||
import com.geeksville.mesh.MeshProtos
|
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
||||||
import com.geeksville.mesh.model.Channel
|
import com.geeksville.mesh.model.Channel
|
||||||
|
@ -50,6 +49,7 @@ fun ImageView.setOpaque() {
|
||||||
class ChannelFragment : ScreenFragment("Channel"), Logging {
|
class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
|
|
||||||
private var _binding: ChannelFragmentBinding? = null
|
private var _binding: ChannelFragmentBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and onDestroyView.
|
// This property is only valid between onCreateView and onDestroyView.
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
@ -81,6 +81,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
val channels = model.channels.value
|
val channels = model.channels.value
|
||||||
val channel = channels?.primaryChannel
|
val channel = channels?.primaryChannel
|
||||||
|
|
||||||
|
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||||
|
|
||||||
|
// Only let buttons work if we are connected to the radio
|
||||||
|
binding.shareButton.isEnabled = connected
|
||||||
|
binding.resetButton.isEnabled = connected
|
||||||
|
|
||||||
binding.editableCheckbox.isChecked = false // start locked
|
binding.editableCheckbox.isChecked = false // start locked
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
binding.qrView.visibility = View.VISIBLE
|
binding.qrView.visibility = View.VISIBLE
|
||||||
|
@ -89,7 +95,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
|
|
||||||
// For now, we only let the user edit/save channels while the radio is awake - because the service
|
// For now, we only let the user edit/save channels while the radio is awake - because the service
|
||||||
// doesn't cache radioconfig writes.
|
// doesn't cache radioconfig writes.
|
||||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
|
||||||
binding.editableCheckbox.isEnabled = connected
|
binding.editableCheckbox.isEnabled = connected
|
||||||
|
|
||||||
binding.qrView.setImageBitmap(channels.getChannelQR())
|
binding.qrView.setImageBitmap(channels.getChannelQR())
|
||||||
|
@ -143,6 +148,28 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send new channel settings to the device
|
||||||
|
private fun installSettings(newChannel: ChannelProtos.ChannelSettings) {
|
||||||
|
val newSet =
|
||||||
|
ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||||
|
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||||
|
try {
|
||||||
|
model.setChannels(newSet)
|
||||||
|
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||||
|
} catch (ex: RemoteException) {
|
||||||
|
errormsg("ignoring channel problem", ex)
|
||||||
|
|
||||||
|
setGUIfromModel() // Throw away user edits
|
||||||
|
|
||||||
|
// Tell the user to try again
|
||||||
|
Snackbar.make(
|
||||||
|
binding.editableCheckbox,
|
||||||
|
R.string.radio_sleeping,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -150,6 +177,21 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
requireActivity().hideKeyboard()
|
requireActivity().hideKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.resetButton.setOnClickListener { _ ->
|
||||||
|
// User just locked it, we should warn and then apply changes to radio
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.reset_to_defaults)
|
||||||
|
.setMessage(R.string.are_you_shure_change_default)
|
||||||
|
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
setGUIfromModel() // throw away any edits
|
||||||
|
}
|
||||||
|
.setPositiveButton(getString(R.string.accept)) { _, _ ->
|
||||||
|
debug("Switching back to default channel")
|
||||||
|
installSettings(Channel.defaultChannel.settings)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
||||||
binding.editableCheckbox.setOnClickListener { _ ->
|
binding.editableCheckbox.setOnClickListener { _ ->
|
||||||
val checked = binding.editableCheckbox.isChecked
|
val checked = binding.editableCheckbox.isChecked
|
||||||
|
@ -178,41 +220,26 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
// Install a new customized channel
|
||||||
|
|
||||||
debug("ASSIGNING NEW AES256 KEY")
|
debug("ASSIGNING NEW AES256 KEY")
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
val bytes = ByteArray(32)
|
val bytes = ByteArray(32)
|
||||||
random.nextBytes(bytes)
|
random.nextBytes(bytes)
|
||||||
newSettings.psk = ByteString.copyFrom(bytes)
|
newSettings.psk = ByteString.copyFrom(bytes)
|
||||||
|
|
||||||
|
val selectedChannelOptionString =
|
||||||
|
binding.filledExposedDropdown.editableText.toString()
|
||||||
|
val modemConfig = getModemConfig(selectedChannelOptionString)
|
||||||
|
|
||||||
|
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
|
||||||
|
newSettings.modemConfig = modemConfig
|
||||||
} else {
|
} else {
|
||||||
debug("Switching back to default channel")
|
debug("Switching back to default channel")
|
||||||
newSettings = Channel.defaultChannel.settings.toBuilder()
|
newSettings = Channel.defaultChannel.settings.toBuilder()
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedChannelOptionString =
|
installSettings(newSettings.build())
|
||||||
binding.filledExposedDropdown.editableText.toString()
|
|
||||||
val modemConfig = getModemConfig(selectedChannelOptionString)
|
|
||||||
|
|
||||||
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
|
|
||||||
newSettings.modemConfig = modemConfig
|
|
||||||
|
|
||||||
val newChannel = newSettings.build()
|
|
||||||
val newSet = ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
|
||||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
|
||||||
try {
|
|
||||||
model.setChannels(newSet)
|
|
||||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
|
||||||
} catch (ex: RemoteException) {
|
|
||||||
errormsg("ignoring channel problem", ex)
|
|
||||||
|
|
||||||
setGUIfromModel() // Throw away user edits
|
|
||||||
|
|
||||||
// Tell the user to try again
|
|
||||||
Snackbar.make(
|
|
||||||
binding.editableCheckbox,
|
|
||||||
R.string.radio_sleeping,
|
|
||||||
Snackbar.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|
|
@ -524,7 +524,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the correct update button configuration based on current progress
|
/// Set the correct update button configuration based on current progress
|
||||||
private fun refreshUpdateButton() {
|
private fun refreshUpdateButton(enable: Boolean) {
|
||||||
debug("Reiniting the udpate button")
|
debug("Reiniting the udpate button")
|
||||||
val info = model.myNodeInfo.value
|
val info = model.myNodeInfo.value
|
||||||
val service = model.meshService
|
val service = model.meshService
|
||||||
|
@ -535,7 +535,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
|
|
||||||
val progress = service.updateStatus
|
val progress = service.updateStatus
|
||||||
|
|
||||||
binding.updateFirmwareButton.isEnabled =
|
binding.updateFirmwareButton.isEnabled = enable &&
|
||||||
(progress < 0) // if currently doing an upgrade disable button
|
(progress < 0) // if currently doing an upgrade disable button
|
||||||
|
|
||||||
if (progress >= 0) {
|
if (progress >= 0) {
|
||||||
|
@ -572,7 +572,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
val connected = model.isConnected.value
|
val connected = model.isConnected.value
|
||||||
|
|
||||||
val isConnected = connected == MeshService.ConnectionState.CONNECTED
|
val isConnected = connected == MeshService.ConnectionState.CONNECTED
|
||||||
binding.nodeSettings.visibility = if(isConnected) View.VISIBLE else View.GONE
|
binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
if (connected == MeshService.ConnectionState.DISCONNECTED)
|
if (connected == MeshService.ConnectionState.DISCONNECTED)
|
||||||
model.ownerName.value = ""
|
model.ownerName.value = ""
|
||||||
|
@ -582,25 +582,19 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
val spinner = binding.regionSpinner
|
val spinner = binding.regionSpinner
|
||||||
val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
|
val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
|
||||||
spinner.onItemSelectedListener = null
|
spinner.onItemSelectedListener = null
|
||||||
if(region != null) {
|
|
||||||
debug("current region is $region")
|
|
||||||
var regionIndex = regions.indexOf(region.name)
|
|
||||||
if(regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
|
||||||
regionIndex = unsetIndex
|
|
||||||
|
|
||||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
debug("current region is $region")
|
||||||
spinner.setSelection(regionIndex, false)
|
var regionIndex = regions.indexOf(region.name)
|
||||||
spinner.onItemSelectedListener = regionSpinnerListener
|
if (regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||||
spinner.isEnabled = true
|
regionIndex = unsetIndex
|
||||||
}
|
|
||||||
else {
|
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||||
warn("region is unset!")
|
spinner.setSelection(regionIndex, false)
|
||||||
spinner.setSelection(unsetIndex, false)
|
spinner.onItemSelectedListener = regionSpinnerListener
|
||||||
spinner.isEnabled = false // leave disabled, because we can't get our region
|
spinner.isEnabled = true
|
||||||
}
|
|
||||||
|
|
||||||
// If actively connected possibly let the user update firmware
|
// If actively connected possibly let the user update firmware
|
||||||
refreshUpdateButton()
|
refreshUpdateButton(region != RadioConfigProtos.RegionCode.Unset)
|
||||||
|
|
||||||
// Update the status string (highest priority messages first)
|
// Update the status string (highest priority messages first)
|
||||||
val info = model.myNodeInfo.value
|
val info = model.myNodeInfo.value
|
||||||
|
@ -620,7 +614,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener{
|
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(
|
override fun onItemSelected(
|
||||||
parent: AdapterView<*>,
|
parent: AdapterView<*>,
|
||||||
view: View,
|
view: View,
|
||||||
|
@ -652,7 +646,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
|
|
||||||
// init our region spinner
|
// init our region spinner
|
||||||
val spinner = binding.regionSpinner
|
val spinner = binding.regionSpinner
|
||||||
val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
val regionAdapter =
|
||||||
|
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
spinner.adapter = regionAdapter
|
spinner.adapter = regionAdapter
|
||||||
|
|
||||||
|
@ -965,7 +960,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
|
|
||||||
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
refreshUpdateButton()
|
refreshUpdateButton(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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="M14.99,4.59V5c0,1.1 -0.9,2 -2,2h-2v2c0,0.55 -0.45,1 -1,1h-2v2h6c0.55,0 1,0.45 1,1v3h1c0.89,0 1.64,0.59 1.9,1.4C19.19,15.98 20,14.08 20,12c0,-3.35 -2.08,-6.23 -5.01,-7.41zM8.99,16v-1l-4.78,-4.78C4.08,10.79 4,11.39 4,12c0,4.07 3.06,7.43 6.99,7.93V18c-1.1,0 -2,-0.9 -2,-2z"
|
||||||
|
android:strokeAlpha="0.3"
|
||||||
|
android:fillAlpha="0.3"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10.99,19.93C7.06,19.43 4,16.07 4,12c0,-0.61 0.08,-1.21 0.21,-1.78L8.99,15v1c0,1.1 0.9,2 2,2v1.93zM17.89,17.4c-0.26,-0.81 -1,-1.4 -1.9,-1.4h-1v-3c0,-0.55 -0.45,-1 -1,-1h-6v-2h2c0.55,0 1,-0.45 1,-1L10.99,7h2c1.1,0 2,-0.9 2,-2v-0.41C17.92,5.77 20,8.65 20,12c0,2.08 -0.81,3.98 -2.11,5.4z"/>
|
||||||
|
</vector>
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
@ -91,30 +92,46 @@
|
||||||
<AutoCompleteTextView
|
<AutoCompleteTextView
|
||||||
android:id="@+id/filled_exposed_dropdown"
|
android:id="@+id/filled_exposed_dropdown"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/set_channel_options" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/resetButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:text="@string/reset"
|
||||||
|
app:icon="@drawable/ic_twotone_public_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/editableCheckbox"
|
android:id="@+id/editableCheckbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="96dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginEnd="32dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:button="@drawable/sl_lock_24dp"
|
android:button="@drawable/sl_lock_24dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/shareButton"
|
app:layout_constraintStart_toEndOf="@+id/resetButton"></CheckBox>
|
||||||
app:layout_constraintStart_toStartOf="parent"></CheckBox>
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/shareButton"
|
android:id="@+id/shareButton"
|
||||||
style="@android:style/Widget.Material.ImageButton"
|
style="@android:style/Widget.Material.ImageButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="96dp"
|
android:layout_marginEnd="32dp"
|
||||||
android:contentDescription="@string/share"
|
android:contentDescription="@string/share"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/editableCheckbox"
|
app:layout_constraintTop_toTopOf="@+id/editableCheckbox"
|
||||||
app:srcCompat="@drawable/ic_twotone_share_24" />
|
app:srcCompat="@drawable/ic_twotone_share_24" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/bottomButtonsGuideline"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_end="16dp" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -97,4 +97,8 @@
|
||||||
<string name="cant_change_no_radio">Couldn\'t change channel, because radio is not yet connected. Please try again.</string>
|
<string name="cant_change_no_radio">Couldn\'t change channel, because radio is not yet connected. Please try again.</string>
|
||||||
<string name="sample_coords">55.332244 34.442211</string>
|
<string name="sample_coords">55.332244 34.442211</string>
|
||||||
<string name="save_messages">Save messages as csv...</string>
|
<string name="save_messages">Save messages as csv...</string>
|
||||||
|
<string name="set_channel_options">Set channel options</string>
|
||||||
|
<string name="reset">Reset</string>
|
||||||
|
<string name="are_you_shure_change_default">Are you sure you want to change to the default channel?</string>
|
||||||
|
<string name="reset_to_defaults">Reset to defaults</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue