diff --git a/app/build.gradle b/app/build.gradle index 51f3447e5..ea6b784fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,13 +23,13 @@ android { } } */ compileSdkVersion 29 - buildToolsVersion "30.0.2" + buildToolsVersion "30.0.1" // Note: 30.0.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 10905 // format is Mmmss (where M is 1+the numeric major number - versionName "0.9.05" + versionCode 10906 // format is Mmmss (where M is 1+the numeric major number + versionName "0.9.06" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -112,15 +112,15 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'com.google.android.material:material:1.2.0' implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' // kotlin serialization implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:1.0-M1-1.4.0-rc" 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 cb8c5c1f8..a1e568d42 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -4,6 +4,7 @@ import android.graphics.Bitmap import android.net.Uri import android.util.Base64 import com.geeksville.mesh.MeshProtos +import com.geeksville.util.anonymize import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter import com.journeyapps.barcodescanner.BarcodeEncoder @@ -32,15 +33,18 @@ data class Channel( .setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build() ) - const val prefix = "https://www.meshtastic.org/c/#" + private const val prefixRoot = "https://www.meshtastic.org/c/" + const val prefix = "$prefixRoot#" private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP private fun urlToSettings(url: Uri): MeshProtos.ChannelSettings { val urlStr = url.toString() - val pathRegex = Regex("$prefix(.*)") + + // Let the path optionally include the # character (or not) so we can work with old URLs generated by old versions of the app + val pathRegex = Regex("$prefixRoot#?(.*)") val (base64) = pathRegex.find(urlStr)?.destructured - ?: throw MalformedURLException("Not a meshtastic URL") + ?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.anonymize(40)}") val bytes = Base64.decode(base64, base64Flags) return MeshProtos.ChannelSettings.parseFrom(bytes) diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt index a2abf1634..6aa081938 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -409,9 +409,12 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String } } } catch (ex: BLEException) { - scheduleReconnect( - "Unexpected error discovering services, forcing disconnect $ex" - ) + if (s.gatt == null) + warn("GATT was closed while discovering, assume we are shutting down") + else + scheduleReconnect( + "Unexpected error discovering services, forcing disconnect $ex" + ) } } } 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 d2e1808fc..8468cbb98 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -29,6 +29,7 @@ import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.ToRadio import com.geeksville.mesh.R import com.geeksville.util.* +import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.* import com.google.protobuf.ByteString @@ -262,20 +263,30 @@ class MeshService : Service(), Logging { locationSettingsResponse.addOnFailureListener { exception -> errormsg("Failed to listen to GPS") - if (exception is ResolvableApiException) { - exceptionReporter { - // Location settings are not satisfied, but this can be fixed - // by showing the user a dialog. - // Show the dialog by calling startResolutionForResult(), - // and check the result in onActivityResult(). - // exception.startResolutionForResult(this@MainActivity, REQUEST_CHECK_SETTINGS) + when (exception) { + is ResolvableApiException -> + exceptionReporter { + // Location settings are not satisfied, but this can be fixed + // by showing the user a dialog. - // For now just punt and show a dialog - warnUserAboutLocation() - } - } else - Exceptions.report(exception) + // Show the dialog by calling startResolutionForResult(), + // and check the result in onActivityResult(). + // exception.startResolutionForResult(this@MainActivity, REQUEST_CHECK_SETTINGS) + + // For now just punt and show a dialog + warnUserAboutLocation() + } + is ApiException -> + if (exception.statusCode == 17) { + // error: cancelled by user + errormsg("User cancelled location access", exception) + } else { + Exceptions.report(exception) + } + else -> + Exceptions.report(exception) + } } val client = LocationServices.getFusedLocationProviderClient(this) diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index 681ec6a4d..69ff4de7e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -32,8 +32,8 @@ fun longBLEUUID(hexFour: String): UUID = UUID.fromString("0000$hexFour-0000-1000 class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) : Logging, Closeable { - /// Timeout before we declare a bluetooth operation failed - var timeoutMsec = 15 * 1000L + /// Timeout before we declare a bluetooth operation failed (used for synchronous API operations only) + var timeoutMsec = 20 * 1000L /// Users can access the GATT directly as needed @Volatile @@ -361,7 +361,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD private fun queueWork( tag: String, cont: Continuation, - timeout: Long = 0, + timeout: Long, initFn: () -> Boolean ) { val btCont = @@ -455,7 +455,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD private fun makeSync(wrappedFn: (SyncContinuation) -> Unit): T { val cont = SyncContinuation() wrappedFn(cont) - return cont.await(timeoutMsec) + return cont.await() // was timeoutMsec but we now do the timeout at the lower BLE level } // Is the gatt trying to repeatedly connect as needed? @@ -495,12 +495,13 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD // Otherwise if you pass in false, it will try to connect now and will timeout and fail in 30 seconds. private fun queueConnect( autoConnect: Boolean = false, - cont: Continuation + cont: Continuation, + timeout: Long = 0 ) { this.autoConnect = autoConnect // assert(gatt == null) this now might be !null with our new reconnect support - queueWork("connect", cont) { + queueWork("connect", cont, timeout) { // Note: To workaround https://issuetracker.google.com/issues/36995652 // Always call BluetoothDevice#connectGatt() with autoConnect=false @@ -585,7 +586,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD } // note - we don't need an init fn (because that would normally redo the connectGatt call - which we don't need) - queueWork("reconnect", CallbackContinuation(cb)) { -> true } + queueWork("reconnect", CallbackContinuation(cb), 0) { -> true } } else { debug("No connectionCallback registered") } @@ -596,20 +597,24 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD private fun queueReadCharacteristic( c: BluetoothGattCharacteristic, - cont: Continuation - ) = queueWork("readC ${c.uuid}", cont) { gatt!!.readCharacteristic(c) } + cont: Continuation, timeout: Long = 0 + ) = queueWork("readC ${c.uuid}", cont, timeout) { gatt!!.readCharacteristic(c) } fun asyncReadCharacteristic( c: BluetoothGattCharacteristic, cb: (Result) -> Unit ) = queueReadCharacteristic(c, CallbackContinuation(cb)) - fun readCharacteristic(c: BluetoothGattCharacteristic): BluetoothGattCharacteristic = - makeSync { queueReadCharacteristic(c, it) } + fun readCharacteristic( + c: BluetoothGattCharacteristic, + timeout: Long = timeoutMsec + ): BluetoothGattCharacteristic = + makeSync { queueReadCharacteristic(c, it, timeout) } - private fun queueDiscoverServices(cont: Continuation) { - queueWork("discover", cont) { - gatt?.discoverServices() ?: throw BLEException("GATT is null") + private fun queueDiscoverServices(cont: Continuation, timeout: Long = 0) { + queueWork("discover", cont, timeout) { + gatt?.discoverServices() + ?: false // throw BLEException("GATT is null") - if we return false here it is probably because the device is being torn down } } @@ -649,8 +654,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD private fun queueWriteCharacteristic( c: BluetoothGattCharacteristic, v: ByteArray, - cont: Continuation - ) = queueWork("writeC ${c.uuid}", cont) { + cont: Continuation, timeout: Long = 0 + ) = queueWork("writeC ${c.uuid}", cont, timeout) { currentReliableWrite = null c.value = v gatt?.writeCharacteristic(c) ?: false @@ -664,17 +669,18 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD fun writeCharacteristic( c: BluetoothGattCharacteristic, - v: ByteArray + v: ByteArray, + timeout: Long = timeoutMsec ): BluetoothGattCharacteristic = - makeSync { queueWriteCharacteristic(c, v, it) } + makeSync { queueWriteCharacteristic(c, v, it, timeout) } /** Like write, but we use the extra reliable flow documented here: * https://stackoverflow.com/questions/24485536/what-is-reliable-write-in-ble */ private fun queueWriteReliable( c: BluetoothGattCharacteristic, - cont: Continuation - ) = queueWork("rwriteC ${c.uuid}", cont) { + cont: Continuation, timeout: Long = 0 + ) = queueWork("rwriteC ${c.uuid}", cont, timeout) { logAssert(gatt!!.beginReliableWrite()) currentReliableWrite = c.value.clone() gatt?.writeCharacteristic(c) ?: false @@ -690,8 +696,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD private fun queueWriteDescriptor( c: BluetoothGattDescriptor, - cont: Continuation - ) = queueWork("writeD", cont) { gatt?.writeDescriptor(c) ?: false } + cont: Continuation, timeout: Long = 0 + ) = queueWork("writeD", cont, timeout) { gatt?.writeDescriptor(c) ?: false } fun asyncWriteDescriptor( c: BluetoothGattDescriptor, @@ -765,7 +771,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD // Cancel any notifications - because when the device comes back it might have forgotten about us notifyHandlers.clear() - + closeGatt() failAllWork(BLEException("Connection closing")) 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 6a132e20c..1efbec740 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -205,7 +205,6 @@ class SoftwareUpdateService : JobIntentService(), Logging { verStringToInt(if (swVer.isEmpty() || swVer == "unset") "99.99.99" else swVer) (curVer > deviceVersion) && (deviceVersion >= minVer) - // true } catch (ex: Exception) { errormsg("Error finding swupdate info", ex) false // If we fail parsing our update info @@ -313,25 +312,30 @@ class SoftwareUpdateService : JobIntentService(), Logging { firmwareNumSent += blockSize } - // We have finished sending all our blocks, so post the CRC so our state machine can advance - val c = firmwareCrc.value - info("Sent all blocks, crc is $c") - sync.writeCharacteristic( - crc32Desc, - toNetworkByteArray(c.toInt(), BluetoothGattCharacteristic.FORMAT_UINT32) - ) + try { + // We have finished sending all our blocks, so post the CRC so our state machine can advance + val c = firmwareCrc.value + info("Sent all blocks, crc is $c") + sync.writeCharacteristic( + crc32Desc, + toNetworkByteArray(c.toInt(), BluetoothGattCharacteristic.FORMAT_UINT32) + ) - // we just read the update result if !0 we have an error - val updateResult = - sync.readCharacteristic(updateResultDesc) - .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0) - if (updateResult != 0) { - progress = -2 - throw Exception("Device update failed, reason=$updateResult") + // we just read the update result if !0 we have an error + val updateResult = + sync.readCharacteristic(updateResultDesc) + .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0) + if (updateResult != 0) { + progress = -2 + throw Exception("Device update failed, reason=$updateResult") + } + + // Device will now reboot + } catch (ex: BLEException) { + // We might get SyncContinuation timeout on the final write, assume the device simply rebooted to run the new load and we missed it + errormsg("Assuming successful update", ex) } - // Device will now reboot - progress = -1 // success } } catch (ex: BLEException) { diff --git a/app/src/main/res/layout/channel_fragment.xml b/app/src/main/res/layout/channel_fragment.xml index d2f411259..305e5d75c 100644 --- a/app/src/main/res/layout/channel_fragment.xml +++ b/app/src/main/res/layout/channel_fragment.xml @@ -31,11 +31,13 @@ --> + + + + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/shareButton" + app:layout_constraintStart_toStartOf="parent"> - - - - - \ No newline at end of file diff --git a/geeksville-androidlib b/geeksville-androidlib index 629a5b162..775502e99 160000 --- a/geeksville-androidlib +++ b/geeksville-androidlib @@ -1 +1 @@ -Subproject commit 629a5b1621d1e79772ddf00290da1edec3a74e10 +Subproject commit 775502e999b06c1e8effbfd631b1924d998b0570