diff --git a/app/build.gradle b/app/build.gradle index 6d4c9c306..8292c32a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,13 +25,14 @@ android { } } */ compileSdkVersion 29 - buildToolsVersion "30.0.2" // Note: 30.0.0.2 doesn't yet work on Github actions CI + // leave undefined to use version plugin wants + // buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI defaultConfig { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 29 - versionCode 20122 // format is Mmmss (where M is 1+the numeric major number - versionName "1.1.22" + versionCode 20133 // format is Mmmss (where M is 1+the numeric major number + versionName "1.1.33" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -107,7 +108,7 @@ protobuf { dependencies { - def room_version = "2.2.5" + def room_version = '2.2.6' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 69e08be69..8bee48f88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -144,6 +144,10 @@ android:scheme="https" android:host="www.meshtastic.org" android:pathPrefix="/c/" /> + diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt index a1afe5edd..f2fa418e6 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt @@ -27,20 +27,27 @@ class MeshUtilApplication : GeeksvilleApplication() { val pref = AppPrefs(this) crashlytics.setUserId(pref.getInstallId()) // be able to group all bugs per anonymous user + // We always send our log messages to the crashlytics lib, but they only get sent to the server if we report an exception + // This makes log messages work properly if someone turns on analytics just before they click report bug. + // send all log messages through crashyltics, so if we do crash we'll have those in the report + val standardLogger = Logging.printlog + Logging.printlog = { level, tag, message -> + crashlytics.log("$tag: $message") + standardLogger(level, tag, message) + } + + fun sendCrashReports() { + if(isAnalyticsAllowed) + crashlytics.sendUnsentReports() + } + + // Send any old reports if user approves + sendCrashReports() + // Attach to our exception wrapper Exceptions.reporter = { exception, _, _ -> crashlytics.recordException(exception) - crashlytics.sendUnsentReports() - } - - if (isAnalyticsAllowed) { - val standardLogger = Logging.printlog - - // send all log messages through crashyltics, so if we do crash we'll have those in the report - Logging.printlog = { level, tag, message -> - crashlytics.log("$tag: $message") - standardLogger(level, tag, message) - } + sendCrashReports() // Send the new report } } diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt index 1af4da1e5..54886c7e5 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -19,6 +19,7 @@ data class Channel( ) { companion object { // Note: this string _SHOULD NOT BE LOCALIZED_ because it directly hashes to values used on the device for the default channel name. + // FIXME - make this work with new channel name system val defaultChannelName = "Default" // These bytes must match the well known and not secret bytes used the default channel AES128 key device code @@ -32,17 +33,17 @@ data class Channel( MeshProtos.ChannelSettings.newBuilder().setName(defaultChannelName) .setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build() ) - + const val prefix = "https://www.meshtastic.org/c/#" - private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING private fun urlToSettings(url: Uri): MeshProtos.ChannelSettings { val urlStr = url.toString() // We no longer support the super old (about 0.8ish? verison of the URLs that don't use the # separator - I doubt // anyone is still using that old format - val pathRegex = Regex("$prefix(.*)") + val pathRegex = Regex("$prefix(.*)", RegexOption.IGNORE_CASE) val (base64) = pathRegex.find(urlStr)?.destructured ?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}") val bytes = Base64.decode(base64, base64Flags) @@ -54,24 +55,39 @@ data class Channel( constructor(url: Uri) : this(urlToSettings(url)) /// Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec - val name: String get() = if(settings.name.isEmpty()) defaultChannelName else settings.name + val name: String + get() = if (settings.name.isEmpty()) { + // We have a new style 'empty' channel name. Use the same logic from the device to convert that to a human readable name + if (settings.bandwidth != 0) + "Unset" + else when (settings.modemConfig) { + MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128 -> "Medium" + MeshProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128 -> "ShortFast" + MeshProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512 -> "LongAlt" + MeshProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096 -> "LongSlow" + else -> "Invalid" + } + } else + settings.name + val modemConfig: MeshProtos.ChannelSettings.ModemConfig get() = settings.modemConfig - val psk get() = if(settings.psk.size() != 1) - settings.psk // A standard PSK - else { - // One of our special 1 byte PSKs, see mesh.proto for docs. - val pskIndex = settings.psk.byteAt(0).toInt() - - if(pskIndex == 0) - ByteString.EMPTY // Treat as an empty PSK (no encryption) + val psk + get() = if (settings.psk.size() != 1) + settings.psk // A standard PSK else { - // Treat an index of 1 as the old channelDefaultKey and work up from there - val bytes = channelDefaultKey.clone() - bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte() - ByteString.copyFrom(bytes) + // One of our special 1 byte PSKs, see mesh.proto for docs. + val pskIndex = settings.psk.byteAt(0).toInt() + + if (pskIndex == 0) + ByteString.EMPTY // Treat as an empty PSK (no encryption) + else { + // Treat an index of 1 as the old channelDefaultKey and work up from there + val bytes = channelDefaultKey.clone() + bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte() + ByteString.copyFrom(bytes) + } } - } /** * Return a name that is formatted as #channename-suffix @@ -80,28 +96,41 @@ data class Channel( */ val humanName: String get() { - val code = settings.psk.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) }) - return "#${settings.name}-${'A' + (code % 26)}" + val suffix: Char = if (settings.psk.size() != 1) { + // we have a full PSK, so hash it to generate the suffix + val code = settings.psk.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) }) + + 'A' + (code % 26) + } else + '0' + settings.psk.byteAt(0).toInt() + + return "#${name}-${suffix}" } /// Can this channel be changed right now? var editable = false /// Return an URL that represents the current channel values - fun getChannelUrl(): Uri { + /// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes + fun getChannelUrl(upperCasePrefix: Boolean = false): Uri { // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs val channelBytes = settings.toByteArray() ?: ByteArray(0) // if unset just use empty val enc = Base64.encodeToString(channelBytes, base64Flags) - return Uri.parse("$prefix$enc") + val p = if(upperCasePrefix) + prefix.toUpperCase() + else + prefix + return Uri.parse("$p$enc") } fun getChannelQR(): Bitmap { val multiFormatWriter = MultiFormatWriter() + // We encode as UPPER case for the QR code URL because QR codes are more efficient for that special case val bitMatrix = - multiFormatWriter.encode(getChannelUrl().toString(), BarcodeFormat.QR_CODE, 192, 192); + multiFormatWriter.encode(getChannelUrl(true).toString(), BarcodeFormat.QR_CODE, 192, 192); val barcodeEncoder = BarcodeEncoder() return barcodeEncoder.createBitmap(bitMatrix) } diff --git a/app/src/main/java/com/geeksville/mesh/service/Constants.kt b/app/src/main/java/com/geeksville/mesh/service/Constants.kt index 4d4b68b9b..3b99c917a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/Constants.kt +++ b/app/src/main/java/com/geeksville/mesh/service/Constants.kt @@ -9,6 +9,7 @@ const val prefix = "com.geeksville.mesh" // a bool true means now connected, false means not const val EXTRA_CONNECTED = "$prefix.Connected" +const val EXTRA_PROGRESS = "$prefix.Progress" /// a bool true means we expect this condition to continue until, false means device might come back const val EXTRA_PERMANENT = "$prefix.Permanent" diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 30d2e8087..bde6821fc 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -23,6 +23,7 @@ import com.geeksville.mesh.MeshProtos.ToRadio import com.geeksville.mesh.database.MeshtasticDatabase import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.entity.Packet +import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted import com.geeksville.util.* import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.ResolvableApiException @@ -891,6 +892,7 @@ class MeshService : Service(), Logging { // Do our startup init try { connectTimeMsec = System.currentTimeMillis() + SoftwareUpdateService.sendProgress(this, ProgressNotStarted) // Kinda crufty way of reiniting software update startConfig() } catch (ex: InvalidProtocolBufferException) { @@ -1097,10 +1099,10 @@ class MeshService : Service(), Logging { DataPair("dev_error_count", myInfo.errorCount) ) - if (myInfo.errorCode != 0) { + if (myInfo.errorCode.number != 0) { GeeksvilleApplication.analytics.track( "dev_error", - DataPair("code", myInfo.errorCode), + DataPair("code", myInfo.errorCode.number), DataPair("address", myInfo.errorAddress), // We also include this info, because it is required to correctly decode address from the map file @@ -1262,7 +1264,12 @@ class MeshService : Service(), Logging { destNum: Int = NODENUM_BROADCAST, wantResponse: Boolean = false ) = serviceScope.handledLaunch { - sendPosition(lat, lon, alt, destNum, wantResponse) + try { + sendPosition(lat, lon, alt, destNum, wantResponse) + } + catch(ex: RadioNotConnectedException) { + warn("Ignoring disconnected radio during gps location update") + } } /** Send our current radio config to the device diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt index d69a4d883..d765c21aa 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt @@ -8,6 +8,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters +import com.geeksville.andlib.BuildConfig import java.util.concurrent.TimeUnit /** @@ -65,6 +66,6 @@ fun MeshService.Companion.startService(context: Context) { // Before binding we want to explicitly create - so the service stays alive forever (so it can keep // listening for the bluetooth packets arriving from the radio. And when they arrive forward them // to Signal or whatever. - info("Trying to start service") + info("Trying to start service debug=${BuildConfig.DEBUG}") requireNotNull(context.startMeshService()) { "Failed to start service" } } diff --git a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt index 69d57639c..84bcfbc1a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -147,6 +147,8 @@ class SoftwareUpdateService : JobIntentService(), Logging { const val ACTION_START_UPDATE = "$prefix.START_UPDATE" + const val ACTION_UPDATE_PROGRESS = "$prefix.UPDATE_PROGRESS" + const val EXTRA_MACADDR = "macaddr" private const val SCAN_PERIOD: Long = 10000 @@ -170,10 +172,15 @@ class SoftwareUpdateService : JobIntentService(), Logging { private val MANUFACTURE_CHARACTER = longBLEUUID("2a29") private val HW_VERSION_CHARACTER = longBLEUUID("2a27") + const val ProgressSuccess = -1 + const val ProgressUpdateFailed = -2 + const val ProgressBleException = -3 + const val ProgressNotStarted = -4 + /** * % progress through the update */ - var progress = 0 + var progress = ProgressNotStarted /** * Convenience method for enqueuing work in to this service. @@ -187,6 +194,17 @@ class SoftwareUpdateService : JobIntentService(), Logging { } + fun sendProgress(context: Context, p: Int) { + if(progress != p) { + progress = p + + val intent = Intent(ACTION_UPDATE_PROGRESS).putExtra( + EXTRA_PROGRESS, + p + ) + context.sendBroadcast(intent) + } + } /** Return true if we thing the firmwarte shoulde be updated * @@ -200,7 +218,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { val minVer = DeviceVersion("0.7.8") // The oldest device version with a working software update service - (curVer > deviceVersion) && (deviceVersion >= minVer) + ((curVer > deviceVersion) && (deviceVersion >= minVer)) } catch (ex: Exception) { errormsg("Error finding swupdate info", ex) false // If we fail parsing our update info @@ -262,7 +280,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { errormsg("Ignoring failure to update spiffs on old appload") } assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) } - progress = -1 // success + sendProgress(context, ProgressSuccess) } // writable region codes in the ESP32 update code @@ -288,7 +306,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { info("Starting firmware update for $assetName, flash region $flashRegion") - progress = 0 + sendProgress(context,0) val totalSizeDesc = getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER) val dataDesc = getCharacteristic(SW_UPDATE_DATA_CHARACTER) val crc32Desc = getCharacteristic(SW_UPDATE_CRC32_CHARACTER) @@ -336,7 +354,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { // yet val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD) 50 else 100 - progress = firmwareNumSent * maxProgress / firmwareSize + sendProgress(context, firmwareNumSent * maxProgress / firmwareSize) debug("sending block ${progress}%") var blockSize = 512 - 3 // Max size MTU excluding framing @@ -366,7 +384,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { sync.readCharacteristic(updateResultDesc) .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0) if (updateResult != 0) { - progress = -2 + sendProgress(context, ProgressUpdateFailed) throw Exception("Device update failed, reason=$updateResult") } @@ -377,7 +395,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { } } } catch (ex: BLEException) { - progress = -3 + sendProgress(context, ProgressBleException) throw ex // Unexpected BLE exception } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 936dfff5f..ec8f2ac53 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -29,7 +29,6 @@ import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.hideKeyboard import com.geeksville.android.isGooglePlayAvailable -import com.geeksville.concurrent.handledLaunch import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R import com.geeksville.mesh.android.bluetoothManager @@ -40,6 +39,9 @@ import com.geeksville.mesh.service.BluetoothInterface import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.RadioInterfaceService import com.geeksville.mesh.service.SerialInterface +import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS +import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted +import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressSuccess import com.geeksville.util.anonymize import com.geeksville.util.exceptionReporter import com.google.android.gms.location.LocationRequest @@ -50,9 +52,9 @@ import com.hoho.android.usbserial.driver.UsbSerialDriver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import java.util.regex.Pattern + object SLogging : Logging {} /// Change to a new macaddr selection, updating GUI and radio @@ -484,27 +486,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { private fun doFirmwareUpdate() { model.meshService?.let { service -> - mainScope.handledLaunch { - debug("User started firmware update") - binding.updateFirmwareButton.isEnabled = false // Disable until things complete - binding.updateProgressBar.visibility = View.VISIBLE - binding.updateProgressBar.progress = 0 // start from scratch + debug("User started firmware update") + binding.updateFirmwareButton.isEnabled = false // Disable until things complete + binding.updateProgressBar.visibility = View.VISIBLE + binding.updateProgressBar.progress = 0 // start from scratch - binding.scanStatusText.text = "Updating firmware, wait up to eight minutes..." - try { - service.startFirmwareUpdate() - while (service.updateStatus >= 0) { - binding.updateProgressBar.progress = service.updateStatus - delay(2000) // Only check occasionally - } - } finally { - val isSuccess = (service.updateStatus == -1) - binding.scanStatusText.text = - if (isSuccess) "Update successful" else "Update failed" - binding.updateProgressBar.isEnabled = false - binding.updateFirmwareButton.isEnabled = !isSuccess - } - } + // We rely on our broadcast receiver to show progress as this progresses + service.startFirmwareUpdate() } } @@ -516,20 +504,55 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { return binding.root } - private fun initNodeInfo() { - val connected = model.isConnected.value - - // If actively connected possibly let the user update firmware + /// Set the correct update button configuration based on current progress + private fun refreshUpdateButton() { + debug("Reiniting the udpate button") val info = model.myNodeInfo.value - if (connected == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate) { + val service = model.meshService + if (model.isConnected.value == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate && service != null) { binding.updateFirmwareButton.visibility = View.VISIBLE - binding.updateFirmwareButton.isEnabled = true binding.updateFirmwareButton.text = getString(R.string.update_to).format(getString(R.string.cur_firmware_version)) + + val progress = service.updateStatus + + binding.updateFirmwareButton.isEnabled = + (progress < 0) // if currently doing an upgrade disable button + + if (progress >= 0) { + binding.updateProgressBar.progress = progress // update partial progress + binding.scanStatusText.setText(R.string.updating_firmware) + binding.updateProgressBar.visibility = View.VISIBLE + } else + when (progress) { + ProgressSuccess -> { + binding.scanStatusText.setText(R.string.update_successful) + binding.updateProgressBar.visibility = View.GONE + } + ProgressNotStarted -> { + // Do nothing - because we don't want to overwrite the status text in this case + binding.updateProgressBar.visibility = View.GONE + } + else -> { + binding.scanStatusText.setText(R.string.update_failed) + binding.updateProgressBar.visibility = View.VISIBLE + } + } + binding.updateProgressBar.isEnabled = false + } else { binding.updateFirmwareButton.visibility = View.GONE binding.updateProgressBar.visibility = View.GONE } + } + + private fun initNodeInfo() { + val connected = model.isConnected.value + + refreshUpdateButton() + + // If actively connected possibly let the user update firmware + val info = model.myNodeInfo.value when (connected) { MeshService.ConnectionState.CONNECTED -> { @@ -784,11 +807,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { initClassicScan() } - override fun onPause() { - super.onPause() - scanModel.stopScan() - } - /** * If the user has not turned on location access throw up a toast warning */ @@ -842,11 +860,29 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } } + private val updateProgressFilter = IntentFilter(ACTION_UPDATE_PROGRESS) + + private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + refreshUpdateButton() + } + } + + override fun onPause() { + super.onPause() + scanModel.stopScan() + + requireActivity().unregisterReceiver(updateProgressReceiver) + } + override fun onResume() { super.onResume() + if (!hasCompanionDeviceApi) scanModel.startScan() + requireActivity().registerReceiver(updateProgressReceiver, updateProgressFilter) + // Keep reminding user BLE is still off val hasUSB = activity?.let { SerialInterface.findDrivers(it).isNotEmpty() } ?: true if (!hasUSB) { diff --git a/app/src/main/proto b/app/src/main/proto index 8729bad7f..dfe7bc121 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 8729bad7f6cfa461be02e3ea65fbde29435b3fe3 +Subproject commit dfe7bc1217a00c23eecb9dfcf1d56fe95ebddc3b diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35e624739..427e2efb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,4 +77,7 @@ Debug Panel 500 last messages Clear + Updating firmware, wait up to eight minutes... + Update successful + Update failed