From 9be189f89c08cf030a8a129f5d3929503c3bbc9a Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 11 Mar 2020 14:45:49 -0700 Subject: [PATCH 01/33] begin adding map view --- TODO.md | 5 ++- app/build.gradle | 3 ++ .../geeksville/mesh/MeshUtilApplication.kt | 4 +++ .../java/com/geeksville/mesh/ui/AppDrawer.kt | 1 + .../main/java/com/geeksville/mesh/ui/Map.kt | 33 +++++++++++++++++++ .../java/com/geeksville/mesh/ui/MeshApp.kt | 1 + .../java/com/geeksville/mesh/ui/Status.kt | 1 + .../main/res/drawable/ic_twotone_map_24.xml | 15 +++++++++ app/src/main/res/values/mapbox-token.xml | 1 + geeksville-androidlib | 2 +- 10 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/Map.kt create mode 100644 app/src/main/res/drawable/ic_twotone_map_24.xml create mode 120000 app/src/main/res/values/mapbox-token.xml diff --git a/TODO.md b/TODO.md index d9a38eb17..b23616cb4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,9 @@ # High priority Work items for soon alpha builds -* update play store listing for public beta +* let channel be editited +* make link sharing work +* finish map view * run services in sim mode on emulator * show offline nodes as greyed out * show time since last contact on the node info card @@ -161,3 +163,4 @@ Don't leave device discoverable. Don't let unpaired users do things with device * generate real channel QR codes * Have play store entry ask users to report if their android version is too old to allow install * use git submodule for androidlib +* update play store listing for public beta \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 11f6cc6a4..b6f46faf7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,6 +87,9 @@ dependencies { //implementation 'com.google.protobuf:protobuf-java:3.11.1' //implementation 'com.google.protobuf:protobuf-java-util:3.11.1' implementation 'com.google.protobuf:protobuf-javalite:3.11.1' + + // mapbox + implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.0.0' // You also need to include the following Compose toolkit dependencies. implementation("androidx.compose:compose-runtime:$compose_version") diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt index a30d7ee0e..3e5d3a0e4 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt @@ -6,6 +6,7 @@ import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.util.Exceptions import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.mapbox.mapboxsdk.Mapbox class MeshUtilApplication : GeeksvilleApplication(null, "58e72ccc361883ea502510baa46580e3") { @@ -26,5 +27,8 @@ class MeshUtilApplication : GeeksvilleApplication(null, "58e72ccc361883ea502510b crashlytics.recordException(exception) } } + + // Mapbox Access token + Mapbox.getInstance(this, getString(R.string.mapbox_access_token)) } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt index ae9f471ae..82da06e7c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt @@ -47,6 +47,7 @@ fun AppDrawer( ScreenButton(Screen.messages) ScreenButton(Screen.users) + ScreenButton(Screen.map) ScreenButton(Screen.channel) ScreenButton(Screen.settings) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt new file mode 100644 index 000000000..52d156d3d --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -0,0 +1,33 @@ +package com.geeksville.mesh.ui + +import androidx.compose.Composable +import androidx.ui.core.ContextAmbient +import androidx.ui.layout.Column +import androidx.ui.layout.LayoutPadding +import androidx.ui.layout.LayoutSize +import androidx.ui.material.MaterialTheme +import androidx.ui.tooling.preview.Preview +import androidx.ui.unit.dp + + +@Composable +fun MapContent() { + analyticsScreen(name = "channel") + + val typography = MaterialTheme.typography() + val context = ContextAmbient.current + + Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) { + + } +} + + +@Preview +@Composable +fun previewMap() { + // another bug? It seems modaldrawerlayout not yet supported in preview + MaterialTheme(colors = palette) { + MapContent() + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index a9c2e2ba6..b1a172e11 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -146,6 +146,7 @@ private fun AppContent(openDrawer: () -> Unit) { Screen.settings -> SettingsContent() Screen.users -> HomeContent() Screen.channel -> ChannelContent() + Screen.map -> MapContent() else -> TODO() } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Status.kt b/app/src/main/java/com/geeksville/mesh/ui/Status.kt index 3d03495e5..dbe4bb64c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Status.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Status.kt @@ -12,6 +12,7 @@ object Screen { val channel = ScreenInfo(R.drawable.ic_twotone_contactless_24, "Channel") val users = ScreenInfo(R.drawable.ic_twotone_people_24, "Users") val messages = ScreenInfo(R.drawable.ic_twotone_message_24, "Messages") + val map = ScreenInfo(R.drawable.ic_twotone_map_24, "Map") } diff --git a/app/src/main/res/drawable/ic_twotone_map_24.xml b/app/src/main/res/drawable/ic_twotone_map_24.xml new file mode 100644 index 000000000..bd96aeb9f --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_map_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/values/mapbox-token.xml b/app/src/main/res/values/mapbox-token.xml new file mode 120000 index 000000000..137035cde --- /dev/null +++ b/app/src/main/res/values/mapbox-token.xml @@ -0,0 +1 @@ +../../../../../../mapbox-token.xml \ No newline at end of file diff --git a/geeksville-androidlib b/geeksville-androidlib index ee0863c3c..188cf4fbb 160000 --- a/geeksville-androidlib +++ b/geeksville-androidlib @@ -1 +1 @@ -Subproject commit ee0863c3c94856f9859d17219761903f4dea00fd +Subproject commit 188cf4fbb503ac0384f1fce4d3d3f0c2c9f07c02 From 01f2d908a42128257d430f09fcfd133e6aa7727a Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 11 Mar 2020 18:13:44 -0700 Subject: [PATCH 02/33] more map wip --- app/build.gradle | 2 +- .../androidx/ui/androidview/ComposedView.kt | 79 +++++++++++++++++++ .../main/java/com/geeksville/mesh/ui/Map.kt | 11 +-- app/src/main/res/layout/map_view.xml | 5 ++ 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/androidx/ui/androidview/ComposedView.kt create mode 100644 app/src/main/res/layout/map_view.xml diff --git a/app/build.gradle b/app/build.gradle index b6f46faf7..2afedd403 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,7 +87,7 @@ dependencies { //implementation 'com.google.protobuf:protobuf-java:3.11.1' //implementation 'com.google.protobuf:protobuf-java-util:3.11.1' implementation 'com.google.protobuf:protobuf-javalite:3.11.1' - + // mapbox implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.0.0' diff --git a/app/src/main/java/androidx/ui/androidview/ComposedView.kt b/app/src/main/java/androidx/ui/androidview/ComposedView.kt new file mode 100644 index 000000000..2c24ae27a --- /dev/null +++ b/app/src/main/java/androidx/ui/androidview/ComposedView.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.ui.androidview + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.annotation.LayoutRes +import androidx.compose.Composable + +/** + * Composes an Android [View] given a layout resource [resId]. The method handles the inflation + * of the [View] and will call the [postInflationCallback] after this happens. Note that the + * callback will always be invoked on the main thread. + * + * @param resId The id of the layout resource to be inflated. + * @param postInflationCallback The callback to be invoked after the layout is inflated. + */ +@Composable +// TODO(popam): support modifiers here +fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) { + AndroidViewHolder( + postInflationCallback = postInflationCallback, + resId = resId + ) +} + +private class AndroidViewHolder(context: Context) : ViewGroup(context) { + var view: View? = null + set(value) { + if (value != field) { + field = value + removeAllViews() + addView(view) + } + } + + var postInflationCallback: (View) -> Unit = {} + + var resId: Int? = null + set(value) { + if (value != field) { + field = value + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(resId!!, this, false) + this.view = view + postInflationCallback(view) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + view?.measure(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + view?.layout(l, t, r, b) + } + + override fun getLayoutParams(): LayoutParams? { + return view?.layoutParams ?: LayoutParams(MATCH_PARENT, MATCH_PARENT) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt index 52d156d3d..92436f0f7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -1,24 +1,21 @@ package com.geeksville.mesh.ui import androidx.compose.Composable +import androidx.ui.androidview.AndroidView import androidx.ui.core.ContextAmbient -import androidx.ui.layout.Column -import androidx.ui.layout.LayoutPadding -import androidx.ui.layout.LayoutSize import androidx.ui.material.MaterialTheme import androidx.ui.tooling.preview.Preview -import androidx.ui.unit.dp +import com.geeksville.mesh.R @Composable fun MapContent() { - analyticsScreen(name = "channel") + analyticsScreen(name = "map") val typography = MaterialTheme.typography() val context = ContextAmbient.current - Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) { - + AndroidView(R.layout.map_view) { } } diff --git a/app/src/main/res/layout/map_view.xml b/app/src/main/res/layout/map_view.xml new file mode 100644 index 000000000..f629a9ee4 --- /dev/null +++ b/app/src/main/res/layout/map_view.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file From 49567562c0eb98ea2c8ce269bf2079545b976183 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 9 Mar 2020 12:51:54 -0700 Subject: [PATCH 03/33] track hw model so we know how many heltec vs ttgo etc --- .../java/com/geeksville/mesh/service/MeshService.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 591d410e3..3930e3d57 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -386,7 +386,7 @@ class MeshService : Service(), Logging { val NODENUM_BROADCAST = 255 // MyNodeInfo sent via special protobuf from radio - data class MyNodeInfo(val myNodeNum: Int, val hasGPS: Boolean) + data class MyNodeInfo(val myNodeNum: Int, val hasGPS: Boolean, val hwModel: String) var myNodeInfo: MyNodeInfo? = null @@ -604,7 +604,7 @@ class MeshService : Service(), Logging { connectedRadio.readMyNode() ) - val mynodeinfo = MyNodeInfo(myInfo.myNodeNum, myInfo.hasGps) + val mynodeinfo = MyNodeInfo(myInfo.myNodeNum, myInfo.hasGps, myInfo.hwModel) myNodeInfo = mynodeinfo // Ask for the current node DB @@ -674,16 +674,21 @@ class MeshService : Service(), Logging { try { reinitFromRadio() + val radioModel = DataPair("radio_model", myNodeInfo?.hwModel ?: "unknown") GeeksvilleApplication.analytics.track( "mesh_connect", DataPair("num_nodes", numNodes), - DataPair("num_online", numOnlineNodes) + DataPair("num_online", numOnlineNodes), + radioModel ) // Once someone connects to hardware start tracking the approximate number of nodes in their mesh // this allows us to collect stats on what typical mesh size is and to tell difference between users who just // downloaded the app, vs has connected it to some hardware. - GeeksvilleApplication.analytics.setUserInfo(DataPair("num_nodes", numNodes)) + GeeksvilleApplication.analytics.setUserInfo( + DataPair("num_nodes", numNodes), + radioModel + ) } catch (ex: RemoteException) { // It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which // causes the phone to try and reconnect. If we fail downloading our initial radio state we don't want to From 6788d8a1c813fe63b9786a38b5ed82363da7a3ce Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 9 Mar 2020 18:54:33 -0700 Subject: [PATCH 04/33] 0.1.4 catch and report a rare? Compose exception kotlin.NullPointerException androidx.ui.core.selection.SelectionManager$handleDragObserver$1.onStart (SelectionManager.kt:184) --- app/build.gradle | 4 ++-- app/src/main/java/com/geeksville/mesh/MainActivity.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2afedd403..694c3959d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 22 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 29 - versionCode 103 - versionName "0.1.3" + versionCode 104 + versionName "0.1.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index d48504094..dd816aca1 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -335,7 +335,7 @@ class MainActivity : AppCompatActivity(), Logging, override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { return try { super.dispatchTouchEvent(ev) - } catch (ex: IllegalStateException) { + } catch (ex: Throwable) { Exceptions.report( ex, "dispatchTouchEvent" From 444485658fd2baeeb5cae2b76ed798a10215724a Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 11 Mar 2020 14:46:02 -0700 Subject: [PATCH 05/33] track # of users with radios --- .../com/geeksville/mesh/service/RadioInterfaceService.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index 6b0e485a8..976b846f4 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -9,6 +9,7 @@ import android.content.Intent import android.os.IBinder import androidx.core.content.edit import com.geeksville.android.BinaryLogFile +import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.concurrent.DeferredExecution import com.geeksville.mesh.IRadioInterfaceService @@ -168,6 +169,12 @@ class RadioInterfaceService : Service(), Logging { putString(DEVADDR_KEY, addr) } + // Record that this use has configured a radio + GeeksvilleApplication.analytics.track( + "mesh_bond" + ) + + // Force the service to reconnect runningService?.let { it.setEnabled(addr != null) } From 9b2a6f3c923882a65d38705539e701d3c6a48270 Mon Sep 17 00:00:00 2001 From: geeksville Date: Thu, 12 Mar 2020 11:58:10 -0700 Subject: [PATCH 06/33] track region/model/firmware version of running devices # Conflicts: # TODO.md # app/src/main/java/com/geeksville/mesh/service/MeshService.kt --- TODO.md | 52 ++++++++++++++++++- .../geeksville/mesh/service/MeshService.kt | 28 ++++++++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index b23616cb4..33c9cae21 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ Work items for soon alpha builds * make link sharing work * finish map view * run services in sim mode on emulator +* track radio brands/regions as a user property (include no-radio as an option) * show offline nodes as greyed out * show time since last contact on the node info card * show pointer arrow on the outside of the user icons, always pointing towoards them @@ -163,4 +164,53 @@ Don't leave device discoverable. Don't let unpaired users do things with device * generate real channel QR codes * Have play store entry ask users to report if their android version is too old to allow install * use git submodule for androidlib -* update play store listing for public beta \ No newline at end of file +* update play store listing for public beta + +Rare bug reproduced: + +D/com.geeksville.mesh.service.SafeBluetooth: work readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 is completed, resuming status=0, res=android.bluetooth.BluetoothGattCharacteristic@f6eb84e +D/com.geeksville.mesh.service.RadioInterfaceService: Received 9 bytes from radio +D/com.geeksville.mesh.service.SafeBluetooth: Enqueuing work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 +D/com.geeksville.mesh.service.SafeBluetooth$BluetoothContinuation: Starting work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 +D/com.geeksville.mesh.service.MeshService: Received broadcast com.geeksville.mesh.RECEIVE_FROMRADIO +E/com.geeksville.util.Exceptions: exceptionReporter Uncaught Exception + com.google.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero). + at com.google.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:1566) + at com.google.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:1655) + at com.geeksville.mesh.MeshProtos$FromRadio.parseFrom(MeshProtos.java:9097) + at com.geeksville.mesh.service.MeshService$radioInterfaceReceiver$1$onReceive$1.invoke(MeshService.kt:742) + at com.geeksville.mesh.service.MeshService$radioInterfaceReceiver$1$onReceive$1.invoke(Unknown Source:0) + at com.geeksville.util.ExceptionsKt.exceptionReporter(Exceptions.kt:31) + at com.geeksville.mesh.service.MeshService$radioInterfaceReceiver$1.onReceive(MeshService.kt:722) + at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1550) + at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2) + at android.os.Handler.handleCallback(Handler.java:883) + at android.os.Handler.dispatchMessage(Handler.java:100) + at android.os.Looper.loop(Looper.java:214) + at android.app.ActivityThread.main(ActivityThread.java:7356) + at java.lang.reflect.Method.invoke(Native Method) + at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) +D/com.geeksville.mesh.service.SafeBluetooth: work readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 is completed, resuming status=0, res=android.bluetooth.BluetoothGattCharacteristic@f6eb84e +D/com.geeksville.mesh.service.RadioInterfaceService: Received 9 bytes from radio +D/com.geeksville.mesh.service.SafeBluetooth: Enqueuing work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 +D/com.geeksville.mesh.service.SafeBluetooth$BluetoothContinuation: Starting work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 +D/com.geeksville.mesh.service.MeshService: Received broadcast com.geeksville.mesh.RECEIVE_FROMRADIO + + +Transition powerFSM transition=Press, from=DARK to=ON +pressing +sending owner !246f28b5367c/Bob use/Bu +Update DB node 0x7c for variant 4, rx_time=0 +old user !246f28b5367c/Bob use/Bu +updating changed=0 user !246f28b5367c/Bob use/Bu +immedate send on mesh (txGood=32,rxGood=0,rxBad=0) +Trigger powerFSM 1 +Transition powerFSM transition=Press, from=ON to=ON +Setting fast framerate +Setting idle framerate +Transition powerFSM transition=Screen-on timeout, from=ON to=DARK + +NOTE: no debug messages on device, though we see in radio interface service we are repeatedly reading FromRadio and getting +the same seven bytes. It sure seems like the old service is still sort of alive... + 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 3930e3d57..6284fec58 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -386,7 +386,13 @@ class MeshService : Service(), Logging { val NODENUM_BROADCAST = 255 // MyNodeInfo sent via special protobuf from radio - data class MyNodeInfo(val myNodeNum: Int, val hasGPS: Boolean, val hwModel: String) + data class MyNodeInfo( + val myNodeNum: Int, + val hasGPS: Boolean, + val region: String, + val model: String, + val firmwareVersion: String + ) var myNodeInfo: MyNodeInfo? = null @@ -604,8 +610,19 @@ class MeshService : Service(), Logging { connectedRadio.readMyNode() ) - val mynodeinfo = MyNodeInfo(myInfo.myNodeNum, myInfo.hasGps, myInfo.hwModel) - myNodeInfo = mynodeinfo + val mi = with(myInfo) { + MyNodeInfo(myNodeNum, hasGps, region, hwModel, firmwareVersion) + } + + myNodeInfo = mi + + /// Track types of devices and firmware versions in use + GeeksvilleApplication.analytics.setUserInfo( + DataPair("region", mi.region), + DataPair("firmware", mi.firmwareVersion), + DataPair("has_gps", mi.hasGPS), + DataPair("hw_model", mi.model) + ) // Ask for the current node DB connectedRadio.restartNodeInfo() @@ -631,7 +648,7 @@ class MeshService : Service(), Logging { // For the local node, it might not be able to update its times because it doesn't have a valid GPS reading yet // so if the info is for _our_ node we always assume time is current val time = - if (it.num == mynodeinfo.myNodeNum) currentSecond() else info.position.time + if (it.num == mi.myNodeNum) currentSecond() else info.position.time it.position = Position( info.position.latitude, @@ -649,6 +666,7 @@ class MeshService : Service(), Logging { onNodeDBChanged() } + /// If we just changed our nodedb, we might want to do somethings private fun onNodeDBChanged() { updateNotification() @@ -674,7 +692,7 @@ class MeshService : Service(), Logging { try { reinitFromRadio() - val radioModel = DataPair("radio_model", myNodeInfo?.hwModel ?: "unknown") + val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown") GeeksvilleApplication.analytics.track( "mesh_connect", DataPair("num_nodes", numNodes), From 3bf285e77f308ba52a1ebfb1aa622f468002e21c Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 3 Mar 2020 11:00:01 -0800 Subject: [PATCH 07/33] fix crashlytics: if user shuts off bluetooth during scan, ignore failure --- app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt index a39423cf3..5104f9054 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt @@ -47,8 +47,8 @@ object ScanState : Logging { debug("stopping scan") try { scanner!!.stopScan(callback) - } catch (ex: IllegalStateException) { - warn("Ignoring error stopping scan, user probably disabled bluetooth: $ex") + } catch (ex: Throwable) { + warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}") } callback = null } From 04720e2f0e758084cfa26d90e8b357af30ea327b Mon Sep 17 00:00:00 2001 From: geeksville Date: Thu, 12 Mar 2020 12:03:40 -0700 Subject: [PATCH 08/33] fix crashlytics: if user shuts off bluetooth during scan, ignore failure # Conflicts: # app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt --- TODO.md | 3 ++- .../mesh/service/{SimRadio.disabled => SimRadio.kt} | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename app/src/main/java/com/geeksville/mesh/service/{SimRadio.disabled => SimRadio.kt} (93%) diff --git a/TODO.md b/TODO.md index 33c9cae21..f8f6a73df 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,6 @@ Work items for soon alpha builds * make link sharing work * finish map view * run services in sim mode on emulator -* track radio brands/regions as a user property (include no-radio as an option) * show offline nodes as greyed out * show time since last contact on the node info card * show pointer arrow on the outside of the user icons, always pointing towoards them @@ -214,3 +213,5 @@ Transition powerFSM transition=Screen-on timeout, from=ON to=DARK NOTE: no debug messages on device, though we see in radio interface service we are repeatedly reading FromRadio and getting the same seven bytes. It sure seems like the old service is still sort of alive... +* track radio brands/regions as a user property (include no-radio as an option) + diff --git a/app/src/main/java/com/geeksville/mesh/service/SimRadio.disabled b/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt similarity index 93% rename from app/src/main/java/com/geeksville/mesh/service/SimRadio.disabled rename to app/src/main/java/com/geeksville/mesh/service/SimRadio.kt index 96822be0d..883308445 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SimRadio.disabled +++ b/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt @@ -1,10 +1,9 @@ package com.geeksville.mesh import android.content.Context -import com.google.protobuf.util.JsonFormat +import com.geeksville.mesh.service.RadioInterfaceService class SimRadio(private val context: Context) { - private val jsonParser = JsonFormat.parser() /** * When simulating we parse these MeshPackets as if they arrived at startup @@ -42,7 +41,7 @@ class SimRadio(private val context: Context) { simInitPackets.forEach { json -> val fromRadio = MeshProtos.FromRadio.newBuilder().apply { packet = MeshProtos.MeshPacket.newBuilder().apply { - jsonParser.merge(json, this) + // jsonParser.merge(json, this) }.build() }.build() From 7e480bef80c9b94ae418dee4f8325d291ec9d2d3 Mon Sep 17 00:00:00 2001 From: geeksville Date: Thu, 12 Mar 2020 12:05:18 -0700 Subject: [PATCH 09/33] sync with dev proto --- geeksville-androidlib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geeksville-androidlib b/geeksville-androidlib index 188cf4fbb..d6f7cba6f 160000 --- a/geeksville-androidlib +++ b/geeksville-androidlib @@ -1 +1 @@ -Subproject commit 188cf4fbb503ac0384f1fce4d3d3f0c2c9f07c02 +Subproject commit d6f7cba6f55a1ff61b97d5b284d649ff4e925386 From 6ce859a952d90d2455574d6b88e9a49ac6645c8c Mon Sep 17 00:00:00 2001 From: geeksville Date: Fri, 13 Mar 2020 16:28:42 -0700 Subject: [PATCH 10/33] hide map WIP for now now --- app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt index 82da06e7c..ea78ed7ac 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt @@ -47,7 +47,7 @@ fun AppDrawer( ScreenButton(Screen.messages) ScreenButton(Screen.users) - ScreenButton(Screen.map) + // ScreenButton(Screen.map) // turn off for now ScreenButton(Screen.channel) ScreenButton(Screen.settings) } From 36b2da72e4c84babb960b05135ebd9c4e5c0f5f3 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 15 Mar 2020 16:30:12 -0700 Subject: [PATCH 11/33] showing real channel data works --- .../java/com/geeksville/mesh/model/UIState.kt | 31 ++- .../com/geeksville/mesh/ui/AndroidImage.kt | 89 +++++++++ .../java/com/geeksville/mesh/ui/Channel.kt | 179 +++++------------- .../java/com/geeksville/mesh/ui/MeshApp.kt | 2 +- app/src/main/proto | 2 +- 5 files changed, 171 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 14c64d2e0..33bd1f7bb 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -10,11 +10,37 @@ import androidx.core.content.edit import com.geeksville.android.Logging import com.geeksville.mesh.IMeshService import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.MeshProtos.ChannelSettings.ModemConfig import com.geeksville.mesh.ui.getInitials import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter import com.journeyapps.barcodescanner.BarcodeEncoder +data class Channel( + val name: String, + val num: Int, + val modemConfig: ModemConfig = ModemConfig.Bw125Cr45Sf128 +) { + companion object { + // Placeholder when emulating + val emulated = Channel("Default", 7) + } + + constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.channelNum, c.modemConfig) { + } +} + +/** + * a nice readable description of modem configs + */ +fun ModemConfig.toHumanString(): String = when (this) { + ModemConfig.Bw125Cr45Sf128 -> "Medium range (but fast)" + ModemConfig.Bw500Cr45Sf128 -> "Short range (but fast)" + ModemConfig.Bw31_25Cr48Sf512 -> "Long range (but slower)" + ModemConfig.Bw125Cr48Sf4096 -> "Very long range (but slow)" + else -> this.toString() +} + /// FIXME - figure out how to merge this staate with the AppStatus Model object UIState : Logging { @@ -34,6 +60,8 @@ object UIState : Logging { /// our activity will read this from prefs or set it to the empty string var ownerName: String = "MrInIDE Ownername" + fun getChannel() = radioConfig.value?.channelSettings?.let { Channel(it) } + /// Return an URL that represents the current channel values fun getChannelUrl(context: Context): String { // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs @@ -52,8 +80,7 @@ object UIState : Logging { } } - fun getChannelQR(context: Context): Bitmap - { + fun getChannelQR(context: Context): Bitmap { val multiFormatWriter = MultiFormatWriter() val bitMatrix = diff --git a/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt b/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt new file mode 100644 index 000000000..b53565b89 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt @@ -0,0 +1,89 @@ +package com.geeksville.mesh.ui + +import android.graphics.Bitmap +import androidx.compose.Composable +import androidx.ui.core.DensityAmbient +import androidx.ui.core.DrawModifier +import androidx.ui.core.Modifier +import androidx.ui.core.toModifier +import androidx.ui.foundation.Box +import androidx.ui.graphics.* +import androidx.ui.graphics.colorspace.ColorSpace +import androidx.ui.graphics.colorspace.ColorSpaces +import androidx.ui.graphics.painter.ImagePainter +import androidx.ui.unit.Density +import androidx.ui.unit.PxSize +import androidx.ui.unit.toRect + +/// Stolen from the Compose SimpleImage, replace with their real Image component someday +// TODO(mount, malkov) : remove when RepaintBoundary is a modifier: b/149982905 +// This is class and not val because if b/149985596 +private object ClipModifier : DrawModifier { + override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) { + canvas.save() + canvas.clipRect(size.toRect()) + drawContent() + canvas.restore() + } +} + + +/// Stolen from the Compose SimpleImage, replace with their real Image component someday +@Composable +fun ScaledImage( + image: Image, + modifier: Modifier = Modifier.None, + tint: Color? = null +) { + with(DensityAmbient.current) { + val imageModifier = ImagePainter(image).toModifier( + scaleFit = ScaleFit.FillMaxDimension, + colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) } + ) + Box(modifier + ClipModifier + imageModifier) + } +} + + +/// Borrowed from Compose +class AndroidImage(val bitmap: Bitmap) : Image { + + /** + * @see Image.width + */ + override val width: Int + get() = bitmap.width + + /** + * @see Image.height + */ + override val height: Int + get() = bitmap.height + + override val config: ImageConfig get() = ImageConfig.Argb8888 + + /** + * @see Image.colorSpace + */ + override val colorSpace: ColorSpace + get() = ColorSpaces.Srgb + + /** + * @see Image.hasAlpha + */ + override val hasAlpha: Boolean + get() = bitmap.hasAlpha() + + /** + * @see Image.nativeImage + */ + override val nativeImage: NativeImage + get() = bitmap + + /** + * @see + */ + override fun prepareToDraw() { + bitmap.prepareToDraw() + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt index 3f289c7a9..2ba40fa66 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt @@ -1,164 +1,87 @@ package com.geeksville.mesh.ui import android.content.Intent -import android.graphics.Bitmap import androidx.compose.Composable -import androidx.ui.core.* -import androidx.ui.foundation.Box -import androidx.ui.graphics.* -import androidx.ui.graphics.colorspace.ColorSpace -import androidx.ui.graphics.colorspace.ColorSpaces -import androidx.ui.graphics.painter.ImagePainter +import androidx.ui.core.ContextAmbient +import androidx.ui.core.Text import androidx.ui.layout.* import androidx.ui.material.MaterialTheme import androidx.ui.material.OutlinedButton import androidx.ui.material.ripple.Ripple import androidx.ui.tooling.preview.Preview -import androidx.ui.unit.Density -import androidx.ui.unit.PxSize import androidx.ui.unit.dp -import androidx.ui.unit.toRect import com.geeksville.analytics.DataPair import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.mesh.R +import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.UIState +import com.geeksville.mesh.model.toHumanString -/// The Compose IDE preview doesn't like the protobufs -data class Channel(val name: String, val num: Int) object ChannelLog : Logging -/// Borrowed from Compose -class AndroidImage(val bitmap: Bitmap) : Image { - - /** - * @see Image.width - */ - override val width: Int - get() = bitmap.width - - /** - * @see Image.height - */ - override val height: Int - get() = bitmap.height - - override val config: ImageConfig get() = ImageConfig.Argb8888 - - /** - * @see Image.colorSpace - */ - override val colorSpace: ColorSpace - get() = ColorSpaces.Srgb - - /** - * @see Image.hasAlpha - */ - override val hasAlpha: Boolean - get() = bitmap.hasAlpha() - - /** - * @see Image.nativeImage - */ - override val nativeImage: NativeImage - get() = bitmap - - /** - * @see - */ - override fun prepareToDraw() { - bitmap.prepareToDraw() - } -} - - -/// Stolen from the Compose SimpleImage, replace with their real Image component someday -// TODO(mount, malkov) : remove when RepaintBoundary is a modifier: b/149982905 -// This is class and not val because if b/149985596 -private object ClipModifier : DrawModifier { - override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) { - canvas.save() - canvas.clipRect(size.toRect()) - drawContent() - canvas.restore() - } -} - -/// Stolen from the Compose SimpleImage, replace with their real Image component someday -@Composable -fun ScaledImage( - image: Image, - modifier: Modifier = Modifier.None, - tint: Color? = null -) { - with(DensityAmbient.current) { - val imageModifier = ImagePainter(image).toModifier( - scaleFit = ScaleFit.FillMaxDimension, - colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) } - ) - Box(modifier + ClipModifier + imageModifier) - } -} @Composable -fun ChannelContent(channel: Channel = Channel("Default", 7)) { +fun ChannelContent(channel: Channel?) { analyticsScreen(name = "channel") val typography = MaterialTheme.typography() val context = ContextAmbient.current Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) { - Text( - text = "Channel: ${channel.name}", - modifier = LayoutGravity.Center, - style = typography.h4 - ) - - Row(modifier = LayoutGravity.Center) { - // simulated qr code - // val image = imageResource(id = R.drawable.qrcode) - val image = AndroidImage(UIState.getChannelQR(context)) - - ScaledImage( - image = image, - modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp) + if (channel != null) { + Text( + text = "Channel: ${channel.name}", + modifier = LayoutGravity.Center, + style = typography.h4 ) - Ripple(bounded = false) { - OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(start = 24.dp), - onClick = { - GeeksvilleApplication.analytics.track( - "share", - DataPair("content_type", "channel") - ) // track how many times users share channels + Row(modifier = LayoutGravity.Center) { + // simulated qr code + // val image = imageResource(id = R.drawable.qrcode) + val image = AndroidImage(UIState.getChannelQR(context)) - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, UIState.getChannelUrl(context)) - putExtra(Intent.EXTRA_TITLE, "A URL for joining a Meshtastic mesh") - type = "text/plain" - } + ScaledImage( + image = image, + modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp) + ) - val shareIntent = Intent.createChooser(sendIntent, null) - context.startActivity(shareIntent) - }) { - VectorImage( - id = R.drawable.ic_twotone_share_24, - tint = palette.onBackground - ) + Ripple(bounded = false) { + OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(start = 24.dp), + onClick = { + GeeksvilleApplication.analytics.track( + "share", + DataPair("content_type", "channel") + ) // track how many times users share channels + + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, UIState.getChannelUrl(context)) + putExtra(Intent.EXTRA_TITLE, "A URL for joining a Meshtastic mesh") + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + }) { + VectorImage( + id = R.drawable.ic_twotone_share_24, + tint = palette.onBackground + ) + } } } - } - Text( - text = "Number: ${channel.num}", - modifier = LayoutGravity.Center - ) - Text( - text = "Mode: Long range (but slow)", - modifier = LayoutGravity.Center - ) + Text( + text = "Number: ${channel.num}", + modifier = LayoutGravity.Center + ) + Text( + text = "Mode: ${channel.modemConfig.toHumanString()}", + modifier = LayoutGravity.Center + ) + } } } @@ -168,6 +91,6 @@ fun ChannelContent(channel: Channel = Channel("Default", 7)) { fun previewChannel() { // another bug? It seems modaldrawerlayout not yet supported in preview MaterialTheme(colors = palette) { - ChannelContent() + ChannelContent(Channel.emulated) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index b1a172e11..600c9ca21 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -145,7 +145,7 @@ private fun AppContent(openDrawer: () -> Unit) { Screen.messages -> MessagesContent() Screen.settings -> SettingsContent() Screen.users -> HomeContent() - Screen.channel -> ChannelContent() + Screen.channel -> ChannelContent(UIState.getChannel()) Screen.map -> MapContent() else -> TODO() } diff --git a/app/src/main/proto b/app/src/main/proto index f309ee8f9..66e926740 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit f309ee8f9e9db37daabd7c76da683e052ef62f7a +Subproject commit 66e926740acb30518d1fdcb901d1cc0b0d48122c From 506796c54b324a530d21b7516dae3bf6eec316af Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 15 Mar 2020 18:44:10 -0700 Subject: [PATCH 12/33] channel editing kinda works --- .../java/com/geeksville/mesh/model/UIState.kt | 29 +++++-- .../java/com/geeksville/mesh/ui/Channel.kt | 82 +++++++++++++------ app/src/main/proto | 2 +- .../main/res/drawable/ic_twotone_lock_24.xml | 15 ++++ .../res/drawable/ic_twotone_lock_open_24.xml | 15 ++++ 5 files changed, 112 insertions(+), 31 deletions(-) create mode 100644 app/src/main/res/drawable/ic_twotone_lock_24.xml create mode 100644 app/src/main/res/drawable/ic_twotone_lock_open_24.xml diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 33bd1f7bb..fe54d37ea 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -5,8 +5,10 @@ import android.content.SharedPreferences import android.graphics.Bitmap import android.os.RemoteException import android.util.Base64 +import androidx.compose.Model import androidx.compose.mutableStateOf import androidx.core.content.edit +import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.Logging import com.geeksville.mesh.IMeshService import com.geeksville.mesh.MeshProtos @@ -16,18 +18,21 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter import com.journeyapps.barcodescanner.BarcodeEncoder +@Model data class Channel( - val name: String, - val num: Int, - val modemConfig: ModemConfig = ModemConfig.Bw125Cr45Sf128 + var name: String, + var modemConfig: ModemConfig ) { companion object { // Placeholder when emulating - val emulated = Channel("Default", 7) + val emulated = Channel("Default", ModemConfig.Bw125Cr45Sf128) } - constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.channelNum, c.modemConfig) { + constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.modemConfig) { } + + /// Can this channel be changed right now? + var editable = false } /** @@ -60,7 +65,19 @@ object UIState : Logging { /// our activity will read this from prefs or set it to the empty string var ownerName: String = "MrInIDE Ownername" - fun getChannel() = radioConfig.value?.channelSettings?.let { Channel(it) } + /** + * Return the current channel info + * FIXME, we should sim channels at the MeshService level if we are running on an emulator, + * for now I just fake it by returning a canned channel. + */ + fun getChannel(): Channel? { + val channel = radioConfig.value?.channelSettings?.let { Channel(it) } + + return if (channel == null && isEmulator) + Channel.emulated + else + channel + } /// Return an URL that represents the current channel values fun getChannelUrl(context: Context): String { diff --git a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt index 2ba40fa66..a55a5c5bf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt @@ -4,10 +4,10 @@ import android.content.Intent import androidx.compose.Composable import androidx.ui.core.ContextAmbient import androidx.ui.core.Text +import androidx.ui.input.ImeAction import androidx.ui.layout.* import androidx.ui.material.MaterialTheme import androidx.ui.material.OutlinedButton -import androidx.ui.material.ripple.Ripple import androidx.ui.tooling.preview.Preview import androidx.ui.unit.dp import com.geeksville.analytics.DataPair @@ -31,24 +31,65 @@ fun ChannelContent(channel: Channel?) { Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) { if (channel != null) { + Row(modifier = LayoutGravity.Center) { + + Text(text = "Channel ", modifier = LayoutGravity.Center) + + if (channel.editable) { + // FIXME - limit to max length + StyledTextField( + value = channel.name, + onValueChange = { channel.name = it }, + textStyle = typography.h4.copy( + color = palette.onSecondary.copy(alpha = 0.8f) + ), + imeAction = ImeAction.Send, + onImeActionPerformed = { + TODO() + } + ) + } else { + Text( + text = channel.name, + style = typography.h4 + ) + } + } + + // simulated qr code + // val image = imageResource(id = R.drawable.qrcode) + val image = AndroidImage(UIState.getChannelQR(context)) + + ScaledImage( + image = image, + modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp) + ) + Text( - text = "Channel: ${channel.name}", - modifier = LayoutGravity.Center, - style = typography.h4 + text = "Mode: ${channel.modemConfig.toHumanString()}", + modifier = LayoutGravity.Center + LayoutPadding(bottom = 16.dp) ) Row(modifier = LayoutGravity.Center) { - // simulated qr code - // val image = imageResource(id = R.drawable.qrcode) - val image = AndroidImage(UIState.getChannelQR(context)) - ScaledImage( - image = image, - modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp) - ) + OutlinedButton(onClick = { + channel.editable = !channel.editable + }) { + if (channel.editable) + VectorImage( + id = R.drawable.ic_twotone_lock_open_24, + tint = palette.onBackground + ) + else + VectorImage( + id = R.drawable.ic_twotone_lock_24, + tint = palette.onBackground + ) + } - Ripple(bounded = false) { - OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(start = 24.dp), + // Only show the share buttone once we are locked + if (!channel.editable) + OutlinedButton(modifier = LayoutPadding(start = 24.dp), onClick = { GeeksvilleApplication.analytics.track( "share", @@ -58,7 +99,10 @@ fun ChannelContent(channel: Channel?) { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, UIState.getChannelUrl(context)) - putExtra(Intent.EXTRA_TITLE, "A URL for joining a Meshtastic mesh") + putExtra( + Intent.EXTRA_TITLE, + "A URL for joining a Meshtastic mesh" + ) type = "text/plain" } @@ -70,17 +114,7 @@ fun ChannelContent(channel: Channel?) { tint = palette.onBackground ) } - } } - - Text( - text = "Number: ${channel.num}", - modifier = LayoutGravity.Center - ) - Text( - text = "Mode: ${channel.modemConfig.toHumanString()}", - modifier = LayoutGravity.Center - ) } } } diff --git a/app/src/main/proto b/app/src/main/proto index 66e926740..398fdf362 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 66e926740acb30518d1fdcb901d1cc0b0d48122c +Subproject commit 398fdf362518e9d6869247cef09f2e071b715639 diff --git a/app/src/main/res/drawable/ic_twotone_lock_24.xml b/app/src/main/res/drawable/ic_twotone_lock_24.xml new file mode 100644 index 000000000..2c454424f --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_lock_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_twotone_lock_open_24.xml b/app/src/main/res/drawable/ic_twotone_lock_open_24.xml new file mode 100644 index 000000000..e17c4ff5d --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_lock_open_24.xml @@ -0,0 +1,15 @@ + + + + From 8812793cbd9abe4241d424702c83e5e253b1eecb Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 15 Mar 2020 21:43:12 -0700 Subject: [PATCH 13/33] remove mixpanel --- TODO.md | 2 +- app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt | 2 +- geeksville-androidlib | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 3737dcd4c..7a12da492 100644 --- a/TODO.md +++ b/TODO.md @@ -73,7 +73,6 @@ rules at the BluetoothDevice level. Either make SafeBluetooth lock at the devic * test with an oldish android release using real hardware * stop using a foreground service * use platform theme (dark or light) -* remove mixpanel analytics * require user auth to pair with the device (i.e. press button on device to allow a new phone to pair with it). Don't leave device discoverable. Don't let unpaired users do things with device * if the rxpacket queue on the device overflows (because android hasn't connected in a while) send a special packet to android which means 'X packets have been dropped because you were offline' -drop oldest packets first @@ -165,6 +164,7 @@ Don't leave device discoverable. Don't let unpaired users do things with device * use git submodule for androidlib * update play store listing for public beta * track radio brands/regions as a user property (include no-radio as an option) +* remove mixpanel analytics Rare bug reproduced: diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt index 3e5d3a0e4..c7e570bdb 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt @@ -9,7 +9,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.mapbox.mapboxsdk.Mapbox -class MeshUtilApplication : GeeksvilleApplication(null, "58e72ccc361883ea502510baa46580e3") { +class MeshUtilApplication : GeeksvilleApplication() { override fun onCreate() { super.onCreate() diff --git a/geeksville-androidlib b/geeksville-androidlib index d6f7cba6f..669bc3fbb 160000 --- a/geeksville-androidlib +++ b/geeksville-androidlib @@ -1 +1 @@ -Subproject commit d6f7cba6f55a1ff61b97d5b284d649ff4e925386 +Subproject commit 669bc3fbb54ffaef3db086d1a756489b781fb2a0 From 40a142064f655bf90d53985b900a638d2be3437b Mon Sep 17 00:00:00 2001 From: geeksville Date: Tue, 17 Mar 2020 11:35:19 -0700 Subject: [PATCH 14/33] channel sharing WIP --- .../java/com/geeksville/mesh/MainActivity.kt | 12 ++- .../java/com/geeksville/mesh/model/UIState.kt | 76 +++++++++++-------- .../java/com/geeksville/mesh/ui/Channel.kt | 7 +- .../com/geeksville/mesh/model/ChannelTest.kt | 14 ++++ 4 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index dd816aca1..0bf71ffdb 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -200,9 +200,6 @@ class MainActivity : AppCompatActivity(), Logging, requestPermission() - setContent { - MeshApp() - } /* not yet working // Configure sign-in to request the user's ID, email address, and basic @@ -221,6 +218,10 @@ class MainActivity : AppCompatActivity(), Logging, // Handle any intent handleIntent(intent) + + setContent { + MeshApp() + } } override fun onNewIntent(intent: Intent) { @@ -233,9 +234,12 @@ class MainActivity : AppCompatActivity(), Logging, val appLinkAction = intent.action val appLinkData: Uri? = intent.data + UIState.requestedChannelUrl = null // assume none + // Were we asked to open one our channel URLs? - if (Intent.ACTION_VIEW == appLinkAction && appLinkData != null) { + if (Intent.ACTION_VIEW == appLinkAction) { debug("Asked to open a channel URL - FIXME, ask user if they want to switch to that channel. If so send the config to the radio") + UIState.requestedChannelUrl = appLinkData } } diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index fe54d37ea..c730cec60 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -3,6 +3,7 @@ package com.geeksville.mesh.model import android.content.Context import android.content.SharedPreferences import android.graphics.Bitmap +import android.net.Uri import android.os.RemoteException import android.util.Base64 import androidx.compose.Model @@ -17,24 +18,61 @@ import com.geeksville.mesh.ui.getInitials import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter import com.journeyapps.barcodescanner.BarcodeEncoder +import java.net.MalformedURLException @Model data class Channel( var name: String, - var modemConfig: ModemConfig + var modemConfig: ModemConfig, + var settings: MeshProtos.ChannelSettings? = null ) { companion object { // Placeholder when emulating val emulated = Channel("Default", ModemConfig.Bw125Cr45Sf128) + + const val prefix = "https://www.meshtastic.org/c/" + + 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(.*)") + val (base64) = pathRegex.find(urlStr)?.destructured + ?: throw MalformedURLException("Not a meshtastic URL") + val bytes = Base64.decode(base64, base64Flags) + + return MeshProtos.ChannelSettings.parseFrom(bytes) + } } - constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.modemConfig) { - } + constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.modemConfig, c) + + constructor(url: Uri) : this(urlToSettings(url)) /// Can this channel be changed right now? var editable = false + + /// Return an URL that represents the current channel values + fun getChannelUrl(): String { + // 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 "$prefix$enc" + } + + fun getChannelQR(): Bitmap { + val multiFormatWriter = MultiFormatWriter() + + val bitMatrix = + multiFormatWriter.encode(getChannelUrl(), BarcodeFormat.QR_CODE, 192, 192); + val barcodeEncoder = BarcodeEncoder() + return barcodeEncoder.createBitmap(bitMatrix) + } } + /** * a nice readable description of modem configs */ @@ -65,6 +103,9 @@ object UIState : Logging { /// our activity will read this from prefs or set it to the empty string var ownerName: String = "MrInIDE Ownername" + /// If the app was launched because we received a new channel intent, the Url will be here + var requestedChannelUrl: Uri? = null + /** * Return the current channel info * FIXME, we should sim channels at the MeshService level if we are running on an emulator, @@ -79,33 +120,6 @@ object UIState : Logging { channel } - /// Return an URL that represents the current channel values - fun getChannelUrl(context: Context): String { - // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs - val radio = radioConfig.value - if (radio != null) { - val settings = radio.channelSettings - val channelBytes = settings.toByteArray() - val enc = Base64.encodeToString(channelBytes, Base64.URL_SAFE + Base64.NO_WRAP) - - return "https://www.meshtastic.org/c/$enc" - } else { - return getPreferences(context).getString( - "owner", - "https://www.meshtastic.org/c/unset" - )!! - } - } - - fun getChannelQR(context: Context): Bitmap { - val multiFormatWriter = MultiFormatWriter() - - val bitMatrix = - multiFormatWriter.encode(getChannelUrl(context), BarcodeFormat.QR_CODE, 192, 192); - val barcodeEncoder = BarcodeEncoder() - return barcodeEncoder.createBitmap(bitMatrix) - } - fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) @@ -114,7 +128,7 @@ object UIState : Logging { radioConfig.value = c getPreferences(context).edit(commit = true) { - this.putString("channel-url", getChannelUrl(context)) + this.putString("channel-url", getChannel()!!.getChannelUrl()) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt index a55a5c5bf..63c0355cb 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt @@ -15,7 +15,6 @@ import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.mesh.R import com.geeksville.mesh.model.Channel -import com.geeksville.mesh.model.UIState import com.geeksville.mesh.model.toHumanString @@ -43,7 +42,7 @@ fun ChannelContent(channel: Channel?) { textStyle = typography.h4.copy( color = palette.onSecondary.copy(alpha = 0.8f) ), - imeAction = ImeAction.Send, + imeAction = ImeAction.Done, onImeActionPerformed = { TODO() } @@ -58,7 +57,7 @@ fun ChannelContent(channel: Channel?) { // simulated qr code // val image = imageResource(id = R.drawable.qrcode) - val image = AndroidImage(UIState.getChannelQR(context)) + val image = AndroidImage(channel.getChannelQR()) ScaledImage( image = image, @@ -98,7 +97,7 @@ fun ChannelContent(channel: Channel?) { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, UIState.getChannelUrl(context)) + putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl()) putExtra( Intent.EXTRA_TITLE, "A URL for joining a Meshtastic mesh" diff --git a/app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt b/app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt new file mode 100644 index 000000000..147086f60 --- /dev/null +++ b/app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt @@ -0,0 +1,14 @@ +package com.geeksville.mesh.model + +import org.junit.Test + +class ChannelTest { + @Test + fun channelUrlGood() { + val ch = Channel.emulated + + // FIXME, currently not allowed because it is a Compose model + // Assert.assertTrue(ch.getChannelUrl().startsWith(Channel.prefix)) + } + +} \ No newline at end of file From bbd76ab75aabd3d6055aa46464c2618d52ab4f29 Mon Sep 17 00:00:00 2001 From: geeksville Date: Tue, 17 Mar 2020 14:56:06 -0700 Subject: [PATCH 15/33] make channel unit tests --- .../java/com/geeksville/mesh/ChannelTest.kt | 22 +++++++++++++++++++ .../mesh/ExampleInstrumentedTest.kt | 8 +++---- .../java/com/geeksville/mesh/model/UIState.kt | 15 ++++++++----- .../com/geeksville/mesh/model/ChannelTest.kt | 14 ------------ 4 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt delete mode 100644 app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt diff --git a/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt b/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt new file mode 100644 index 000000000..02e3cf459 --- /dev/null +++ b/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt @@ -0,0 +1,22 @@ +package com.geeksville.mesh + + +import androidx.compose.frames.open +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.geeksville.mesh.model.Channel +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ChannelTest { + @Test + fun channelUrlGood() { + open() // Needed to make Compose think we are inside a Frame + val ch = Channel.emulated + + Assert.assertTrue(ch.getChannelUrl().toString().startsWith(Channel.prefix)) + Assert.assertEquals(Channel(ch.getChannelUrl()), ch) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt index 2ae926b86..2e96d2f76 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.geeksville.mesh -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -19,6 +17,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.geeksville.com.geeksville.mesh", appContext.packageName) + assertEquals("com.geeksville.mesh", appContext.packageName) } } diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index c730cec60..36c6fc195 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -24,11 +24,14 @@ import java.net.MalformedURLException data class Channel( var name: String, var modemConfig: ModemConfig, - var settings: MeshProtos.ChannelSettings? = null + var settings: MeshProtos.ChannelSettings? = MeshProtos.ChannelSettings.getDefaultInstance() ) { companion object { // Placeholder when emulating - val emulated = Channel("Default", ModemConfig.Bw125Cr45Sf128) + val emulated = Channel( + MeshProtos.ChannelSettings.newBuilder().setName("Default") + .setModemConfig(ModemConfig.Bw125Cr45Sf128).build() + ) const val prefix = "https://www.meshtastic.org/c/" @@ -53,20 +56,20 @@ data class Channel( var editable = false /// Return an URL that represents the current channel values - fun getChannelUrl(): String { + fun getChannelUrl(): 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 "$prefix$enc" + return Uri.parse("$prefix$enc") } fun getChannelQR(): Bitmap { val multiFormatWriter = MultiFormatWriter() val bitMatrix = - multiFormatWriter.encode(getChannelUrl(), BarcodeFormat.QR_CODE, 192, 192); + multiFormatWriter.encode(getChannelUrl().toString(), BarcodeFormat.QR_CODE, 192, 192); val barcodeEncoder = BarcodeEncoder() return barcodeEncoder.createBitmap(bitMatrix) } @@ -128,7 +131,7 @@ object UIState : Logging { radioConfig.value = c getPreferences(context).edit(commit = true) { - this.putString("channel-url", getChannel()!!.getChannelUrl()) + this.putString("channel-url", getChannel()!!.getChannelUrl().toString()) } } diff --git a/app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt b/app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt deleted file mode 100644 index 147086f60..000000000 --- a/app/src/test/java/com/geeksville/mesh/model/ChannelTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.geeksville.mesh.model - -import org.junit.Test - -class ChannelTest { - @Test - fun channelUrlGood() { - val ch = Channel.emulated - - // FIXME, currently not allowed because it is a Compose model - // Assert.assertTrue(ch.getChannelUrl().startsWith(Channel.prefix)) - } - -} \ No newline at end of file From 28f488a3943c2509e89472e7bdf8c67112f286fb Mon Sep 17 00:00:00 2001 From: geeksville Date: Tue, 24 Mar 2020 13:48:00 -0700 Subject: [PATCH 16/33] log device errors via analytics related to https://github.com/meshtastic/Meshtastic-esp32/issues/53 --- .../com/geeksville/mesh/service/MeshService.kt | 16 +++++++++++++++- app/src/main/proto | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) 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 6284fec58..3f2e60b7f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -621,9 +621,23 @@ class MeshService : Service(), Logging { DataPair("region", mi.region), DataPair("firmware", mi.firmwareVersion), DataPair("has_gps", mi.hasGPS), - DataPair("hw_model", mi.model) + DataPair("hw_model", mi.model), + DataPair("dev_error_count", myInfo.errorCount) ) + if (myInfo.errorCode != 0) { + GeeksvilleApplication.analytics.track( + "dev_error", + DataPair("code", myInfo.errorCode), + DataPair("address", myInfo.errorAddress), + + // We also include this info, because it is required to correctly decode address from the map file + DataPair("firmware", mi.firmwareVersion), + DataPair("hw_model", mi.model), + DataPair("region", mi.region) + ) + } + // Ask for the current node DB connectedRadio.restartNodeInfo() diff --git a/app/src/main/proto b/app/src/main/proto index 398fdf362..1b2449b50 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 398fdf362518e9d6869247cef09f2e071b715639 +Subproject commit 1b2449b50d11f66d90511559e94cdf40f525fafb From b085a7f761502d3daba1fd8f6c7e405083b4e7c7 Mon Sep 17 00:00:00 2001 From: geeksville Date: Tue, 24 Mar 2020 13:48:22 -0700 Subject: [PATCH 17/33] split Channel into its own file --- .../java/com/geeksville/mesh/model/Channel.kt | 79 +++++++++++++++++++ .../java/com/geeksville/mesh/model/UIState.kt | 75 ------------------ 2 files changed, 79 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/model/Channel.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt new file mode 100644 index 000000000..ce3e1cc5f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -0,0 +1,79 @@ +package com.geeksville.mesh.model + +import android.graphics.Bitmap +import android.net.Uri +import android.util.Base64 +import androidx.compose.Model +import com.geeksville.mesh.MeshProtos +import com.google.zxing.BarcodeFormat +import com.google.zxing.MultiFormatWriter +import com.journeyapps.barcodescanner.BarcodeEncoder +import java.net.MalformedURLException + + +@Model +data class Channel( + var name: String, + var modemConfig: MeshProtos.ChannelSettings.ModemConfig, + var settings: MeshProtos.ChannelSettings? = MeshProtos.ChannelSettings.getDefaultInstance() +) { + companion object { + // Placeholder when emulating + val emulated = Channel( + MeshProtos.ChannelSettings.newBuilder().setName("Default") + .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 fun urlToSettings(url: Uri): MeshProtos.ChannelSettings { + val urlStr = url.toString() + val pathRegex = Regex("$prefix(.*)") + val (base64) = pathRegex.find(urlStr)?.destructured + ?: throw MalformedURLException("Not a meshtastic URL") + val bytes = Base64.decode(base64, base64Flags) + + return MeshProtos.ChannelSettings.parseFrom(bytes) + } + } + + constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.modemConfig, c) + + constructor(url: Uri) : this(urlToSettings(url)) + + /// Can this channel be changed right now? + var editable = false + + /// Return an URL that represents the current channel values + fun getChannelUrl(): 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") + } + + fun getChannelQR(): Bitmap { + val multiFormatWriter = MultiFormatWriter() + + val bitMatrix = + multiFormatWriter.encode(getChannelUrl().toString(), BarcodeFormat.QR_CODE, 192, 192); + val barcodeEncoder = BarcodeEncoder() + return barcodeEncoder.createBitmap(bitMatrix) + } +} + + +/** + * a nice readable description of modem configs + */ +fun MeshProtos.ChannelSettings.ModemConfig.toHumanString(): String = when (this) { + MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128 -> "Medium range (but fast)" + MeshProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128 -> "Short range (but fast)" + MeshProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512 -> "Long range (but slower)" + MeshProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096 -> "Very long range (but slow)" + else -> this.toString() +} diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 36c6fc195..3cc5a120d 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -2,90 +2,15 @@ package com.geeksville.mesh.model import android.content.Context import android.content.SharedPreferences -import android.graphics.Bitmap import android.net.Uri import android.os.RemoteException -import android.util.Base64 -import androidx.compose.Model import androidx.compose.mutableStateOf import androidx.core.content.edit import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.Logging import com.geeksville.mesh.IMeshService import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.MeshProtos.ChannelSettings.ModemConfig import com.geeksville.mesh.ui.getInitials -import com.google.zxing.BarcodeFormat -import com.google.zxing.MultiFormatWriter -import com.journeyapps.barcodescanner.BarcodeEncoder -import java.net.MalformedURLException - -@Model -data class Channel( - var name: String, - var modemConfig: ModemConfig, - var settings: MeshProtos.ChannelSettings? = MeshProtos.ChannelSettings.getDefaultInstance() -) { - companion object { - // Placeholder when emulating - val emulated = Channel( - MeshProtos.ChannelSettings.newBuilder().setName("Default") - .setModemConfig(ModemConfig.Bw125Cr45Sf128).build() - ) - - const val prefix = "https://www.meshtastic.org/c/" - - 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(.*)") - val (base64) = pathRegex.find(urlStr)?.destructured - ?: throw MalformedURLException("Not a meshtastic URL") - val bytes = Base64.decode(base64, base64Flags) - - return MeshProtos.ChannelSettings.parseFrom(bytes) - } - } - - constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.modemConfig, c) - - constructor(url: Uri) : this(urlToSettings(url)) - - /// Can this channel be changed right now? - var editable = false - - /// Return an URL that represents the current channel values - fun getChannelUrl(): 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") - } - - fun getChannelQR(): Bitmap { - val multiFormatWriter = MultiFormatWriter() - - val bitMatrix = - multiFormatWriter.encode(getChannelUrl().toString(), BarcodeFormat.QR_CODE, 192, 192); - val barcodeEncoder = BarcodeEncoder() - return barcodeEncoder.createBitmap(bitMatrix) - } -} - - -/** - * a nice readable description of modem configs - */ -fun ModemConfig.toHumanString(): String = when (this) { - ModemConfig.Bw125Cr45Sf128 -> "Medium range (but fast)" - ModemConfig.Bw500Cr45Sf128 -> "Short range (but fast)" - ModemConfig.Bw31_25Cr48Sf512 -> "Long range (but slower)" - ModemConfig.Bw125Cr48Sf4096 -> "Very long range (but slow)" - else -> this.toString() -} /// FIXME - figure out how to merge this staate with the AppStatus Model object UIState : Logging { From 5b60253e0030b219ea4b5b75047209fb60a262cb Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 29 Mar 2020 11:49:35 -0700 Subject: [PATCH 18/33] update ide to canary 4 --- .idea/compiler.xml | 2 ++ .idea/gradle.xml | 1 + build.gradle | 2 +- geeksville-androidlib | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1bd955209..551d479b0 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -5,6 +5,8 @@ + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 54874b2cf..dba811c87 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -16,6 +16,7 @@ diff --git a/build.gradle b/build.gradle index c40721560..c8ed30bba 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0-alpha01' + classpath 'com.android.tools.build:gradle:4.1.0-alpha04' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/geeksville-androidlib b/geeksville-androidlib index 669bc3fbb..65f39f90c 160000 --- a/geeksville-androidlib +++ b/geeksville-androidlib @@ -1 +1 @@ -Subproject commit 669bc3fbb54ffaef3db086d1a756489b781fb2a0 +Subproject commit 65f39f90ce365263620d5f9cbddca0c8abebcf9a diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be5f8b888..eb5bc3dcd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Feb 27 12:08:19 PST 2020 +#Sun Mar 29 11:25:06 PDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-bin.zip From 40853723ab35c1486593b970adbad2efc69239a1 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sun, 29 Mar 2020 13:38:50 -0700 Subject: [PATCH 19/33] update to dev07 compose --- .../java/androidx/ui/androidview/ComposedView.kt | 10 ++++++---- .../java/com/geeksville/mesh/ui/AndroidImage.kt | 15 +++++++-------- .../main/java/com/geeksville/mesh/ui/AppDrawer.kt | 2 +- .../java/com/geeksville/mesh/ui/BTScanScreen.kt | 4 ++-- .../main/java/com/geeksville/mesh/ui/MeshApp.kt | 1 - .../main/java/com/geeksville/mesh/ui/Messages.kt | 2 +- .../java/com/geeksville/mesh/ui/NodeInfoCard.kt | 3 +-- .../com/geeksville/mesh/ui/StyledTextField.kt | 8 ++++---- .../main/java/com/geeksville/mesh/ui/Vectors.kt | 5 ++--- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 11 files changed, 27 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/androidx/ui/androidview/ComposedView.kt b/app/src/main/java/androidx/ui/androidview/ComposedView.kt index 2c24ae27a..3f1c218af 100644 --- a/app/src/main/java/androidx/ui/androidview/ComposedView.kt +++ b/app/src/main/java/androidx/ui/androidview/ComposedView.kt @@ -23,6 +23,7 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.annotation.LayoutRes import androidx.compose.Composable +import androidx.ui.core.ContextAmbient /** * Composes an Android [View] given a layout resource [resId]. The method handles the inflation @@ -35,10 +36,11 @@ import androidx.compose.Composable @Composable // TODO(popam): support modifiers here fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) { - AndroidViewHolder( - postInflationCallback = postInflationCallback, - resId = resId - ) + val context = ContextAmbient.current + + val r = AndroidViewHolder(context) + r.postInflationCallback = postInflationCallback + r.resId = resId } private class AndroidViewHolder(context: Context) : ViewGroup(context) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt b/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt index b53565b89..85daaf0c8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt @@ -5,10 +5,9 @@ import androidx.compose.Composable import androidx.ui.core.DensityAmbient import androidx.ui.core.DrawModifier import androidx.ui.core.Modifier -import androidx.ui.core.toModifier +import androidx.ui.core.asModifier import androidx.ui.foundation.Box import androidx.ui.graphics.* -import androidx.ui.graphics.colorspace.ColorSpace import androidx.ui.graphics.colorspace.ColorSpaces import androidx.ui.graphics.painter.ImagePainter import androidx.ui.unit.Density @@ -31,12 +30,12 @@ private object ClipModifier : DrawModifier { /// Stolen from the Compose SimpleImage, replace with their real Image component someday @Composable fun ScaledImage( - image: Image, + image: ImageAsset, modifier: Modifier = Modifier.None, tint: Color? = null ) { with(DensityAmbient.current) { - val imageModifier = ImagePainter(image).toModifier( + val imageModifier = ImagePainter(image).asModifier( scaleFit = ScaleFit.FillMaxDimension, colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) } ) @@ -46,7 +45,7 @@ fun ScaledImage( /// Borrowed from Compose -class AndroidImage(val bitmap: Bitmap) : Image { +class AndroidImage(val bitmap: Bitmap) : ImageAsset { /** * @see Image.width @@ -60,12 +59,12 @@ class AndroidImage(val bitmap: Bitmap) : Image { override val height: Int get() = bitmap.height - override val config: ImageConfig get() = ImageConfig.Argb8888 + override val config: ImageAssetConfig get() = ImageAssetConfig.Argb8888 /** * @see Image.colorSpace */ - override val colorSpace: ColorSpace + override val colorSpace: androidx.ui.graphics.colorspace.ColorSpace get() = ColorSpaces.Srgb /** @@ -77,7 +76,7 @@ class AndroidImage(val bitmap: Bitmap) : Image { /** * @see Image.nativeImage */ - override val nativeImage: NativeImage + override val nativeImage: NativeImageAsset get() = bitmap /** diff --git a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt index ea78ed7ac..616cf9ded 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt @@ -9,8 +9,8 @@ import androidx.ui.graphics.Color import androidx.ui.layout.* import androidx.ui.material.Divider import androidx.ui.material.MaterialTheme +import androidx.ui.material.Surface import androidx.ui.material.TextButton -import androidx.ui.material.surface.Surface import androidx.ui.tooling.preview.Preview import androidx.ui.unit.dp import com.geeksville.mesh.R diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt index 5104f9054..0bffee9f2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt @@ -14,7 +14,7 @@ import androidx.ui.core.Text import androidx.ui.layout.Column import androidx.ui.layout.LayoutGravity import androidx.ui.material.CircularProgressIndicator -import androidx.ui.material.EmphasisLevels +import androidx.ui.material.MaterialTheme import androidx.ui.material.ProvideEmphasis import androidx.ui.material.RadioGroup import androidx.ui.tooling.preview.Preview @@ -173,7 +173,7 @@ fun BTScanScreen() { Column { ScanUIState.devices.values.forEach { // disabled pending https://issuetracker.google.com/issues/149528535 - ProvideEmphasis(emphasis = if (it.bonded) EmphasisLevels().high else EmphasisLevels().disabled) { + ProvideEmphasis(emphasis = if (it.bonded) MaterialTheme.emphasisLevels().high else MaterialTheme.emphasisLevels().disabled) { RadioGroupTextItem( selected = (it.isSelected), onSelect = { diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index 600c9ca21..f756e3a89 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -9,7 +9,6 @@ import androidx.ui.layout.Container import androidx.ui.layout.LayoutSize import androidx.ui.layout.Row import androidx.ui.material.* -import androidx.ui.material.surface.Surface import androidx.ui.tooling.preview.Preview import androidx.ui.unit.dp import com.geeksville.android.Logging diff --git a/app/src/main/java/com/geeksville/mesh/ui/Messages.kt b/app/src/main/java/com/geeksville/mesh/ui/Messages.kt index 1e4b56857..04256ab1a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Messages.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Messages.kt @@ -75,7 +75,7 @@ fun MessagesContent() { val topPad = 4.dp VerticalScroller( - modifier = LayoutFlexible(1f) + modifier = LayoutWeight(1f) ) { Column { messages.forEach { msg -> diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt index cc17f427d..eccfc19f5 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt @@ -3,7 +3,6 @@ package com.geeksville.mesh.ui import androidx.compose.Composable import androidx.ui.core.Text import androidx.ui.layout.* -import androidx.ui.material.EmphasisLevels import androidx.ui.material.MaterialTheme import androidx.ui.material.ProvideEmphasis import androidx.ui.tooling.preview.Preview @@ -47,7 +46,7 @@ fun CompassHeading(modifier: Modifier1 = Modifier1.None, node: NodeInfo) { @Composable fun NodeHeading(node: NodeInfo) { - ProvideEmphasis(emphasis = EmphasisLevels().high) { + ProvideEmphasis(emphasis = MaterialTheme.emphasisLevels().high) { Text( node.user?.longName ?: "unknown", style = MaterialTheme.typography().subtitle1 diff --git a/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt b/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt index 70a64994f..449fd53c8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt @@ -11,9 +11,9 @@ import androidx.ui.input.KeyboardType import androidx.ui.input.VisualTransformation import androidx.ui.layout.LayoutPadding import androidx.ui.material.Emphasis -import androidx.ui.material.EmphasisLevels +import androidx.ui.material.MaterialTheme import androidx.ui.material.ProvideEmphasis -import androidx.ui.material.surface.Surface +import androidx.ui.material.Surface import androidx.ui.text.TextStyle import androidx.ui.unit.dp @@ -30,7 +30,7 @@ fun StyledTextField( value: String, modifier: Modifier = Modifier.None, onValueChange: (String) -> Unit = {}, - textStyle: TextStyle? = null, + textStyle: TextStyle = TextStyle.Default, keyboardType: KeyboardType = KeyboardType.Text, imeAction: ImeAction = ImeAction.Unspecified, onFocus: () -> Unit = {}, @@ -47,7 +47,7 @@ fun StyledTextField( shape = RoundedCornerShape(4.dp) ) { val showingHint = state { value.isEmpty() } - val level = if (showingHint.value) HintEmphasis else EmphasisLevels().medium + val level = if (showingHint.value) HintEmphasis else MaterialTheme.emphasisLevels().medium ProvideEmphasis(level) { TextField( diff --git a/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt b/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt index 22fb0f5d3..9aa4fb313 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt @@ -5,7 +5,7 @@ import androidx.compose.Composable import androidx.ui.core.Modifier import androidx.ui.foundation.Icon import androidx.ui.graphics.Color -import androidx.ui.graphics.vector.DrawVector +import androidx.ui.graphics.vector.drawVector import androidx.ui.layout.Container import androidx.ui.layout.LayoutSize import androidx.ui.material.IconButton @@ -42,9 +42,8 @@ fun VectorImage( modifier = modifier + LayoutSize( vector.defaultWidth, vector.defaultHeight - ) + ) + drawVector(vector, tint) ) { - DrawVector(vector, tint) } // } } diff --git a/build.gradle b/build.gradle index c8ed30bba..1af1d03f6 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '1.3.61' - ext.compose_version = '0.1.0-dev06' + ext.compose_version = '0.1.0-dev07' repositories { google() jcenter() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eb5bc3dcd..d3c6a75e0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Mar 29 11:25:06 PDT 2020 +#Sun Mar 29 12:13:52 PDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-all.zip From d9ef6815ec204fb8da37d2a7e250f109c909c95f Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 10:26:16 -0700 Subject: [PATCH 20/33] back to making maps work? --- .../ComposedView.kt | 6 +++++- .../java/com/geeksville/mesh/MainActivity.kt | 1 + .../java/com/geeksville/mesh/model/UIState.kt | 3 +++ .../java/com/geeksville/mesh/ui/AppDrawer.kt | 2 +- .../main/java/com/geeksville/mesh/ui/Map.kt | 18 ++++++++++++++++-- 5 files changed, 26 insertions(+), 4 deletions(-) rename app/src/main/java/androidx/ui/{androidview => fakeandroidview}/ComposedView.kt (94%) diff --git a/app/src/main/java/androidx/ui/androidview/ComposedView.kt b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt similarity index 94% rename from app/src/main/java/androidx/ui/androidview/ComposedView.kt rename to app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt index 3f1c218af..05558c3e0 100644 --- a/app/src/main/java/androidx/ui/androidview/ComposedView.kt +++ b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.ui.androidview +package androidx.ui.fakeandroidview import android.content.Context import android.view.LayoutInflater @@ -41,8 +41,12 @@ fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { val r = AndroidViewHolder(context) r.postInflationCallback = postInflationCallback r.resId = resId + + // Hmm - how is merely creating an AndroidViewHolder sufficient to have it end up in the + // activities view hierarchy? } + private class AndroidViewHolder(context: Context) : ViewGroup(context) { var view: View? = null set(value) { diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 0bf71ffdb..cf7a7c0b3 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -185,6 +185,7 @@ class MainActivity : AppCompatActivity(), Logging, val prefs = UIState.getPreferences(this) UIState.ownerName = prefs.getString("owner", "")!! UIState.meshService = null + UIState.savedInstanceState = savedInstanceState // Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 3cc5a120d..9e509046e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -3,6 +3,7 @@ package com.geeksville.mesh.model import android.content.Context import android.content.SharedPreferences import android.net.Uri +import android.os.Bundle import android.os.RemoteException import androidx.compose.mutableStateOf import androidx.core.content.edit @@ -34,6 +35,8 @@ object UIState : Logging { /// If the app was launched because we received a new channel intent, the Url will be here var requestedChannelUrl: Uri? = null + var savedInstanceState: Bundle? = null + /** * Return the current channel info * FIXME, we should sim channels at the MeshService level if we are running on an emulator, diff --git a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt index 616cf9ded..c6a9694b8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt @@ -47,7 +47,7 @@ fun AppDrawer( ScreenButton(Screen.messages) ScreenButton(Screen.users) - // ScreenButton(Screen.map) // turn off for now + ScreenButton(Screen.map) // turn off for now ScreenButton(Screen.channel) ScreenButton(Screen.settings) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt index 92436f0f7..93b8b4cee 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -1,12 +1,18 @@ package com.geeksville.mesh.ui import androidx.compose.Composable -import androidx.ui.androidview.AndroidView import androidx.ui.core.ContextAmbient +import androidx.ui.core.Text +import androidx.ui.fakeandroidview.AndroidView +import androidx.ui.layout.Column import androidx.ui.material.MaterialTheme import androidx.ui.tooling.preview.Preview +import com.geeksville.android.Logging import com.geeksville.mesh.R +import com.geeksville.mesh.model.UIState +import com.mapbox.mapboxsdk.maps.MapView +object mapLog : Logging @Composable fun MapContent() { @@ -15,7 +21,15 @@ fun MapContent() { val typography = MaterialTheme.typography() val context = ContextAmbient.current - AndroidView(R.layout.map_view) { + Column { + Text("hi") + AndroidView(R.layout.map_view) { view -> + view as MapView + view.onCreate(UIState.savedInstanceState) + view.getMapAsync { + mapLog.info("In getmap") + } + } } } From 4e7d59f77519a700d2a964b7c953cdd7511c25bb Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 11:03:45 -0700 Subject: [PATCH 21/33] change to use Scaffold per Compose geeks recommendations --- .../java/com/geeksville/mesh/ui/MeshApp.kt | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index f756e3a89..b0313b1e0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -126,28 +126,27 @@ fun previewView() { private fun AppContent(openDrawer: () -> Unit) { // crossfade breaks onCommit behavior because it keeps old views around //Crossfade(AppStatus.currentScreen) { screen -> - Surface(color = (MaterialTheme.colors()).background) { + //Surface(color = (MaterialTheme.colors()).background) { - Column { - TopAppBar( - title = { Text(text = "Meshtastic") }, - navigationIcon = { - Container(LayoutSize(40.dp, 40.dp)) { - VectorImageButton(R.drawable.ic_launcher_new_foreground) { - openDrawer() - } + Scaffold(topAppBar = { + TopAppBar( + title = { Text(text = "Meshtastic") }, + navigationIcon = { + Container(LayoutSize(40.dp, 40.dp)) { + VectorImageButton(R.drawable.ic_launcher_new_foreground) { + openDrawer() } } - ) - - when (AppStatus.currentScreen) { - Screen.messages -> MessagesContent() - Screen.settings -> SettingsContent() - Screen.users -> HomeContent() - Screen.channel -> ChannelContent(UIState.getChannel()) - Screen.map -> MapContent() - else -> TODO() } + ) + }) { + when (AppStatus.currentScreen) { + Screen.messages -> MessagesContent() + Screen.settings -> SettingsContent() + Screen.users -> HomeContent() + Screen.channel -> ChannelContent(UIState.getChannel()) + Screen.map -> MapContent() + else -> TODO() } } //} From ecef170004ed4e6e5ff0cb3544db3c60dbe060e2 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 11:56:59 -0700 Subject: [PATCH 22/33] map kinda works --- app/build.gradle | 2 +- .../ui/fakeandroidview/ComposedView.kt | 10 +-- .../main/java/com/geeksville/mesh/ui/Map.kt | 68 +++++++++++++++++-- app/src/main/res/layout/map_view.xml | 11 ++- 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 694c3959d..56ea0e0ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,7 +45,7 @@ android { composeOptions { kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129" - kotlinCompilerExtensionVersion "0.1.0-dev06" + kotlinCompilerExtensionVersion "0.1.0-dev07" } } diff --git a/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt index 05558c3e0..211a957f5 100644 --- a/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt +++ b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt @@ -23,7 +23,6 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.annotation.LayoutRes import androidx.compose.Composable -import androidx.ui.core.ContextAmbient /** * Composes an Android [View] given a layout resource [resId]. The method handles the inflation @@ -36,14 +35,7 @@ import androidx.ui.core.ContextAmbient @Composable // TODO(popam): support modifiers here fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) { - val context = ContextAmbient.current - - val r = AndroidViewHolder(context) - r.postInflationCallback = postInflationCallback - r.resId = resId - - // Hmm - how is merely creating an AndroidViewHolder sufficient to have it end up in the - // activities view hierarchy? + AndroidViewHolder(postInflationCallback = postInflationCallback, resId = resId) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt index 93b8b4cee..040852657 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -1,8 +1,11 @@ package com.geeksville.mesh.ui +import android.app.Activity +import android.app.Application +import android.os.Bundle import androidx.compose.Composable +import androidx.compose.onCommit import androidx.ui.core.ContextAmbient -import androidx.ui.core.Text import androidx.ui.fakeandroidview.AndroidView import androidx.ui.layout.Column import androidx.ui.material.MaterialTheme @@ -11,9 +14,49 @@ import com.geeksville.android.Logging import com.geeksville.mesh.R import com.geeksville.mesh.model.UIState import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.Style object mapLog : Logging + +/** + * mapbox requires this, until compose has a nicer way of doing it, do it here + */ +private val mapLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks { + var view: MapView? = null + + override fun onActivityPaused(activity: Activity) { + view!!.onPause() + } + + override fun onActivityStarted(activity: Activity) { + view!!.onStart() + } + + override fun onActivityDestroyed(activity: Activity) { + view!!.onDestroy() + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + view!!.onSaveInstanceState(outState) + } + + override fun onActivityStopped(activity: Activity) { + view!!.onStop() + } + + /** + * Called when the Activity calls [super.onCreate()][Activity.onCreate]. + */ + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityResumed(activity: Activity) { + view!!.onResume() + } +} + + @Composable fun MapContent() { analyticsScreen(name = "map") @@ -21,13 +64,30 @@ fun MapContent() { val typography = MaterialTheme.typography() val context = ContextAmbient.current + onCommit(AppStatus.currentScreen) { + onDispose { + // We no longer care about activity lifecycle + (context.applicationContext as Application).unregisterActivityLifecycleCallbacks( + mapLifecycleCallbacks + ) + mapLifecycleCallbacks.view = null + } + } + Column { - Text("hi") AndroidView(R.layout.map_view) { view -> view as MapView view.onCreate(UIState.savedInstanceState) - view.getMapAsync { - mapLog.info("In getmap") + + mapLifecycleCallbacks.view = view + (context.applicationContext as Application).registerActivityLifecycleCallbacks( + mapLifecycleCallbacks + ) + + view.getMapAsync { map -> + map.setStyle(Style.OUTDOORS) { + // Map is set up and the style has loaded. Now you can add data or make other map adjustments + } } } } diff --git a/app/src/main/res/layout/map_view.xml b/app/src/main/res/layout/map_view.xml index f629a9ee4..f9bc993e0 100644 --- a/app/src/main/res/layout/map_view.xml +++ b/app/src/main/res/layout/map_view.xml @@ -1,5 +1,12 @@ - \ No newline at end of file + mapbox:mapbox_uiZoomGestures="true" + mapbox:mapbox_uiScrollGestures="true" + mapbox:mapbox_cameraTargetLat="-32.557013" + mapbox:mapbox_cameraTargetLng="-56.149056" + mapbox:mapbox_cameraZoom="5.526846"> From 915bd837edec4423556a748089d40f5f35b2ec1a Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 12:47:01 -0700 Subject: [PATCH 23/33] adding map markers kinda works --- .../main/java/com/geeksville/mesh/ui/Map.kt | 64 +++++++++++++++---- .../res/drawable/ic_twotone_person_pin_24.xml | 15 +++++ 2 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/drawable/ic_twotone_person_pin_24.xml diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt index 040852657..6420892cc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -7,14 +7,22 @@ import androidx.compose.Composable import androidx.compose.onCommit import androidx.ui.core.ContextAmbient import androidx.ui.fakeandroidview.AndroidView -import androidx.ui.layout.Column import androidx.ui.material.MaterialTheme import androidx.ui.tooling.preview.Preview import com.geeksville.android.Logging import com.geeksville.mesh.R +import com.geeksville.mesh.model.NodeDB import com.geeksville.mesh.model.UIState +import com.mapbox.geojson.Feature +import com.mapbox.geojson.FeatureCollection +import com.mapbox.geojson.Point import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.style.layers.Property +import com.mapbox.mapboxsdk.style.layers.PropertyFactory +import com.mapbox.mapboxsdk.style.layers.SymbolLayer +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource + object mapLog : Logging @@ -74,23 +82,53 @@ fun MapContent() { } } - Column { - AndroidView(R.layout.map_view) { view -> - view as MapView - view.onCreate(UIState.savedInstanceState) - - mapLifecycleCallbacks.view = view - (context.applicationContext as Application).registerActivityLifecycleCallbacks( - mapLifecycleCallbacks + // Find all nodes with valid locations + val locations = NodeDB.nodes.values.mapNotNull { node -> + val p = node.position + if (p != null && (p.latitude != 0.0 || p.longitude != 0.0)) + Feature.fromGeometry( + Point.fromLngLat( + p.latitude, + p.longitude + ) ) + else + null + } + val nodeSourceId = "node-positions" + val nodeLayerId = "node-layer" + val markerImageId = "my-marker-image" + val nodePositions = + GeoJsonSource(nodeSourceId, FeatureCollection.fromFeatures(locations)) - view.getMapAsync { map -> - map.setStyle(Style.OUTDOORS) { - // Map is set up and the style has loaded. Now you can add data or make other map adjustments - } + // val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24) + val markerIcon = context.getDrawable(R.drawable.ic_twotone_person_pin_24)!! + + val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId) + nodeLayer.setProperties( + PropertyFactory.iconImage(markerImageId), + PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM) + ) + + //Column { + AndroidView(R.layout.map_view) { view -> + view as MapView + view.onCreate(UIState.savedInstanceState) + + mapLifecycleCallbacks.view = view + (context.applicationContext as Application).registerActivityLifecycleCallbacks( + mapLifecycleCallbacks + ) + + view.getMapAsync { map -> + map.setStyle(Style.OUTDOORS) { style -> + style.addImage(markerImageId, markerIcon) + style.addSource(nodePositions) + style.addLayer(nodeLayer) } } } + //} } diff --git a/app/src/main/res/drawable/ic_twotone_person_pin_24.xml b/app/src/main/res/drawable/ic_twotone_person_pin_24.xml new file mode 100644 index 000000000..407fb4f98 --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_person_pin_24.xml @@ -0,0 +1,15 @@ + + + + From 5e188cfdaaea08056a9653c124ba23ff76f28c9e Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 13:06:41 -0700 Subject: [PATCH 24/33] zooming to user position works --- app/src/main/java/com/geeksville/mesh/ui/Map.kt | 11 +++++++++++ app/src/main/res/layout/map_view.xml | 5 +---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt index 6420892cc..6bef3b9c8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -16,6 +16,9 @@ import com.geeksville.mesh.model.UIState import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.Point +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory +import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.style.layers.Property @@ -126,6 +129,14 @@ fun MapContent() { style.addSource(nodePositions) style.addLayer(nodeLayer) } + + // Center on the user's position (if we have it) + NodeDB.ourNodeInfo?.position?.let { + val cameraPos = CameraPosition.Builder().target( + LatLng(it.latitude, it.longitude) + ).zoom(8.0).build() + map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPos), 1000) + } } } //} diff --git a/app/src/main/res/layout/map_view.xml b/app/src/main/res/layout/map_view.xml index f9bc993e0..554c64b3a 100644 --- a/app/src/main/res/layout/map_view.xml +++ b/app/src/main/res/layout/map_view.xml @@ -6,7 +6,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" mapbox:mapbox_uiZoomGestures="true" - mapbox:mapbox_uiScrollGestures="true" - mapbox:mapbox_cameraTargetLat="-32.557013" - mapbox:mapbox_cameraTargetLng="-56.149056" - mapbox:mapbox_cameraZoom="5.526846"> + mapbox:mapbox_uiScrollGestures="true"> From 4a0331c62e4baafa54fdab5a5ad2bf4d961be079 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 13:45:14 -0700 Subject: [PATCH 25/33] transfer copyright --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19ad47268..7859aeadd 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,6 @@ for verbose logging: adb shell setprop log.tag.FA VERBOSE ``` -Copyright 2018, S. Kevin Hester-Chow, kevinh@geeksville.com. GPL V3 license +Copyright 2019, Geeksville Industries, LLC. GPL V3 license From 4bc94da22406366d1855753d9859e5654d0cdfe7 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 15:00:18 -0700 Subject: [PATCH 26/33] done with map view for now --- .../ui/fakeandroidview/ComposedView.kt | 27 +++++++++++++ .../main/java/com/geeksville/mesh/ui/Map.kt | 39 +++++++++++++------ app/src/main/res/layout/map_view.xml | 1 + 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt index 211a957f5..0c0e7e4aa 100644 --- a/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt +++ b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt @@ -18,6 +18,7 @@ package androidx.ui.fakeandroidview import android.content.Context import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -62,6 +63,10 @@ private class AndroidViewHolder(context: Context) : ViewGroup(context) { } } + init { + isClickable = true + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { view?.measure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0) @@ -74,4 +79,26 @@ private class AndroidViewHolder(context: Context) : ViewGroup(context) { override fun getLayoutParams(): LayoutParams? { return view?.layoutParams ?: LayoutParams(MATCH_PARENT, MATCH_PARENT) } + + /** + * Implement this method to handle touch screen motion events. + * + * + * If this method is used to detect click actions, it is recommended that + * the actions be performed by implementing and calling + * [.performClick]. This will ensure consistent system behavior, + * including: + * + * * obeying click sound preferences + * * dispatching OnClickListener calls + * * handling [ACTION_CLICK][AccessibilityNodeInfo.ACTION_CLICK] when + * accessibility features are enabled + * + * + * @param event The motion event. + * @return True if the event was handled, false otherwise. + */ + override fun onTouchEvent(event: MotionEvent?): Boolean { + return super.onTouchEvent(event) + } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt index 6bef3b9c8..19a721a41 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt @@ -2,6 +2,7 @@ package com.geeksville.mesh.ui import android.app.Activity import android.app.Application +import android.graphics.Color import android.os.Bundle import androidx.compose.Composable import androidx.compose.onCommit @@ -21,8 +22,12 @@ import com.mapbox.mapboxsdk.camera.CameraUpdateFactory import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.style.expressions.Expression import com.mapbox.mapboxsdk.style.layers.Property +import com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_TOP +import com.mapbox.mapboxsdk.style.layers.Property.TEXT_JUSTIFY_AUTO import com.mapbox.mapboxsdk.style.layers.PropertyFactory +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.* import com.mapbox.mapboxsdk.style.layers.SymbolLayer import com.mapbox.mapboxsdk.style.sources.GeoJsonSource @@ -88,18 +93,21 @@ fun MapContent() { // Find all nodes with valid locations val locations = NodeDB.nodes.values.mapNotNull { node -> val p = node.position - if (p != null && (p.latitude != 0.0 || p.longitude != 0.0)) - Feature.fromGeometry( + if (p != null && (p.latitude != 0.0 || p.longitude != 0.0)) { + val f = Feature.fromGeometry( Point.fromLngLat( - p.latitude, - p.longitude + p.longitude, + p.latitude ) ) - else + node.user?.let { f.addStringProperty("name", it.longName) } + f + } else null } val nodeSourceId = "node-positions" val nodeLayerId = "node-layer" + val labelLayerId = "label-layer" val markerImageId = "my-marker-image" val nodePositions = GeoJsonSource(nodeSourceId, FeatureCollection.fromFeatures(locations)) @@ -107,13 +115,19 @@ fun MapContent() { // val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24) val markerIcon = context.getDrawable(R.drawable.ic_twotone_person_pin_24)!! - val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId) - nodeLayer.setProperties( + val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId).withProperties( PropertyFactory.iconImage(markerImageId), PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM) ) - //Column { + val labelLayer = SymbolLayer(labelLayerId, nodeSourceId).withProperties( + textField(Expression.get("name")), + textSize(12f), + textColor(Color.RED), + textVariableAnchor(arrayOf(TEXT_ANCHOR_TOP)), + textJustify(TEXT_JUSTIFY_AUTO) + ) + AndroidView(R.layout.map_view) { view -> view as MapView view.onCreate(UIState.savedInstanceState) @@ -125,21 +139,24 @@ fun MapContent() { view.getMapAsync { map -> map.setStyle(Style.OUTDOORS) { style -> - style.addImage(markerImageId, markerIcon) style.addSource(nodePositions) + style.addImage(markerImageId, markerIcon) style.addLayer(nodeLayer) + style.addLayer(labelLayer) } + //map.uiSettings.isScrollGesturesEnabled = true + //map.uiSettings.isZoomGesturesEnabled = true + // Center on the user's position (if we have it) NodeDB.ourNodeInfo?.position?.let { val cameraPos = CameraPosition.Builder().target( LatLng(it.latitude, it.longitude) - ).zoom(8.0).build() + ).zoom(9.0).build() map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPos), 1000) } } } - //} } diff --git a/app/src/main/res/layout/map_view.xml b/app/src/main/res/layout/map_view.xml index 554c64b3a..a77c3ecf9 100644 --- a/app/src/main/res/layout/map_view.xml +++ b/app/src/main/res/layout/map_view.xml @@ -5,5 +5,6 @@ android:id="@+id/mapView" android:layout_width="match_parent" android:layout_height="match_parent" + android:clickable="true" mapbox:mapbox_uiZoomGestures="true" mapbox:mapbox_uiScrollGestures="true"> From 81a12831484ff5a6f77414ccd4f4f66d56861ff9 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 16:44:48 -0700 Subject: [PATCH 27/33] Android can cache BLE service descriptors. But our device descriptors can change still (because 'progress'). So force android to reread those descriptors while we are in alpha. --- .../mesh/service/RadioInterfaceService.kt | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index 976b846f4..6a3f4ea46 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -13,7 +13,9 @@ import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.concurrent.DeferredExecution import com.geeksville.mesh.IRadioInterfaceService +import com.geeksville.util.exceptionReporter import com.geeksville.util.toRemoteExceptions +import java.lang.reflect.Method import java.util.* @@ -268,12 +270,27 @@ class RadioInterfaceService : Service(), Logging { isConnected = false } + /** + * Android caches old services. But our service is still changing often, so force it to reread the service definitions every + * time + */ + private fun forceServiceRefresh() { + exceptionReporter { + // BluetoothGatt gatt + val gatt = safe!!.gatt!! + val refresh: Method = gatt.javaClass.getMethod("refresh") + refresh.invoke(gatt) + } + } + private fun onConnect(connRes: Result) { // This callback is invoked after we are connected connRes.getOrThrow() // FIXME, instead just try to reconnect? info("Connected to radio!") + forceServiceRefresh() + // FIXME - no need to discover services more than once - instead use lazy() to use them in future attempts safe!!.asyncDiscoverServices { discRes -> discRes.getOrThrow() // FIXME, instead just try to reconnect? @@ -434,13 +451,16 @@ class RadioInterfaceService : Service(), Logging { // A write of any size to nodeinfo means restart reading override fun restartNodeInfo() = doWrite(BTM_NODEINFO_CHARACTER, ByteArray(0)) - override fun readMyNode() = doRead(BTM_MYNODE_CHARACTER)!! + override fun readMyNode() = + doRead(BTM_MYNODE_CHARACTER) ?: throw Exception("Device returned empty MyNodeInfo") override fun sendToRadio(a: ByteArray) = handleSendToRadio(a) - override fun readRadioConfig() = doRead(BTM_RADIO_CHARACTER)!! + override fun readRadioConfig() = + doRead(BTM_RADIO_CHARACTER) ?: throw Exception("Device returned empty RadioConfig") - override fun readOwner() = doRead(BTM_OWNER_CHARACTER)!! + override fun readOwner() = + doRead(BTM_OWNER_CHARACTER) ?: throw Exception("Device returned empty Owner") override fun writeOwner(owner: ByteArray) = doWrite(BTM_OWNER_CHARACTER, owner) From f1681582ed6e815ab3f851ed2c552aa6414cb25a Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 16:45:09 -0700 Subject: [PATCH 28/33] update ble scan GUI if bond state changes --- app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt index 0bffee9f2..1811faafc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt @@ -89,11 +89,13 @@ fun BTScanScreen() { val addr = result.device.address // prevent logspam because weill get get lots of redundant scan results - if (!ScanUIState.devices.contains(addr)) { + val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED + val oldEntry = ScanUIState.devices[addr] + if (oldEntry == null || oldEntry.bonded != isBonded) { val entry = BTScanEntry( result.device.name, addr, - result.device.bondState == BluetoothDevice.BOND_BONDED + isBonded ) ScanState.debug("onScanResult ${entry}") ScanUIState.devices[addr] = entry From 3330c7af2112fed5460497208a77cbb5371466a1 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 16:46:12 -0700 Subject: [PATCH 29/33] send URIs as strings when in an android parcel --- app/src/main/java/com/geeksville/mesh/ui/Channel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt index 63c0355cb..0bcad65ff 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt @@ -97,7 +97,7 @@ fun ChannelContent(channel: Channel?) { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl()) + putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl().toString()) putExtra( Intent.EXTRA_TITLE, "A URL for joining a Meshtastic mesh" From 088ba687d48cbab325dbc27769642a380aee060b Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 17:35:33 -0700 Subject: [PATCH 30/33] if we receive packets early in app start, wait to process them till we have a nodedb --- TODO.md | 51 +------------------ .../geeksville/mesh/service/MeshService.kt | 29 +++++++++-- 2 files changed, 27 insertions(+), 53 deletions(-) diff --git a/TODO.md b/TODO.md index 7a12da492..b7371a95b 100644 --- a/TODO.md +++ b/TODO.md @@ -30,7 +30,6 @@ the channel is encrypted, you can share the the channel key with others by qr co * when a text arrives, move that node info card to the bottom on the window - put the text to the left of the card. with a small arrow/distance/shortname * let the user type texts somewhere * use this for preferences? https://developer.android.com/guide/topics/ui/settings/ -* at connect we might receive messages before finished downloading the nodeinfo. In that case, process those messages later * test with oldest compatible android in emulator (see below for testing with hardware) * add play store link with https://developers.google.com/analytics/devguides/collection/android/v4/campaigns#google-play-url-builder and the play icon @@ -165,53 +164,5 @@ Don't leave device discoverable. Don't let unpaired users do things with device * update play store listing for public beta * track radio brands/regions as a user property (include no-radio as an option) * remove mixpanel analytics - -Rare bug reproduced: - -D/com.geeksville.mesh.service.SafeBluetooth: work readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 is completed, resuming status=0, res=android.bluetooth.BluetoothGattCharacteristic@f6eb84e -D/com.geeksville.mesh.service.RadioInterfaceService: Received 9 bytes from radio -D/com.geeksville.mesh.service.SafeBluetooth: Enqueuing work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 -D/com.geeksville.mesh.service.SafeBluetooth$BluetoothContinuation: Starting work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 -D/com.geeksville.mesh.service.MeshService: Received broadcast com.geeksville.mesh.RECEIVE_FROMRADIO -E/com.geeksville.util.Exceptions: exceptionReporter Uncaught Exception - com.google.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero). - at com.google.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:1566) - at com.google.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:1655) - at com.geeksville.mesh.MeshProtos$FromRadio.parseFrom(MeshProtos.java:9097) - at com.geeksville.mesh.service.MeshService$radioInterfaceReceiver$1$onReceive$1.invoke(MeshService.kt:742) - at com.geeksville.mesh.service.MeshService$radioInterfaceReceiver$1$onReceive$1.invoke(Unknown Source:0) - at com.geeksville.util.ExceptionsKt.exceptionReporter(Exceptions.kt:31) - at com.geeksville.mesh.service.MeshService$radioInterfaceReceiver$1.onReceive(MeshService.kt:722) - at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1550) - at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2) - at android.os.Handler.handleCallback(Handler.java:883) - at android.os.Handler.dispatchMessage(Handler.java:100) - at android.os.Looper.loop(Looper.java:214) - at android.app.ActivityThread.main(ActivityThread.java:7356) - at java.lang.reflect.Method.invoke(Native Method) - at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) - at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) -D/com.geeksville.mesh.service.SafeBluetooth: work readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 is completed, resuming status=0, res=android.bluetooth.BluetoothGattCharacteristic@f6eb84e -D/com.geeksville.mesh.service.RadioInterfaceService: Received 9 bytes from radio -D/com.geeksville.mesh.service.SafeBluetooth: Enqueuing work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 -D/com.geeksville.mesh.service.SafeBluetooth$BluetoothContinuation: Starting work: readC 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 -D/com.geeksville.mesh.service.MeshService: Received broadcast com.geeksville.mesh.RECEIVE_FROMRADIO - - -Transition powerFSM transition=Press, from=DARK to=ON -pressing -sending owner !246f28b5367c/Bob use/Bu -Update DB node 0x7c for variant 4, rx_time=0 -old user !246f28b5367c/Bob use/Bu -updating changed=0 user !246f28b5367c/Bob use/Bu -immedate send on mesh (txGood=32,rxGood=0,rxBad=0) -Trigger powerFSM 1 -Transition powerFSM transition=Press, from=ON to=ON -Setting fast framerate -Setting idle framerate -Transition powerFSM transition=Screen-on timeout, from=ON to=DARK - -NOTE: no debug messages on device, though we see in radio interface service we are repeatedly reading FromRadio and getting -the same seven bytes. It sure seems like the old service is still sort of alive... - +* at connect we might receive messages before finished downloading the nodeinfo. In that case, process those messages later 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 3f2e60b7f..14c192903 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -399,6 +399,9 @@ class MeshService : Service(), Logging { /// Is our radio connected to the phone? private var isConnected = false + /// True after we've done our initial node db init + private var haveNodeDB = false + // The database of active nodes, index is the node number private val nodeDBbyNodeNum = mutableMapOf() @@ -563,8 +566,28 @@ class MeshService : Service(), Logging { } } + /// If packets arrive before we have our node DB, we delay parsing them until the DB is ready + private val earlyPackets = mutableListOf() + /// Update our model and resend as needed for a MeshPacket we just received from the radio private fun handleReceivedMeshPacket(packet: MeshPacket) { + if (haveNodeDB) { + processReceivedMeshPacket(packet) + onNodeDBChanged() + } else { + earlyPackets.add(packet) + logAssert(earlyPackets.size < 128) // The max should normally be about 32, but if the device is messed up it might try to send forever + } + } + + /// Process any packets that showed up too early + private fun processEarlyPackets() { + earlyPackets.forEach { processReceivedMeshPacket(it) } + earlyPackets.clear() + } + + /// Update our model and resend as needed for a MeshPacket we just received from the radio + private fun processReceivedMeshPacket(packet: MeshPacket) { val fromNum = packet.from // FIXME, perhaps we could learn our node ID by looking at any to packets the radio @@ -597,8 +620,6 @@ class MeshService : Service(), Logging { handleReceivedUser(fromNum, p.user) else -> TODO("Unexpected SubPacket variant") } - - onNodeDBChanged() } private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt() @@ -677,10 +698,12 @@ class MeshService : Service(), Logging { infoBytes = connectedRadio.readNodeInfo() } + haveNodeDB = true // we've done our initial node db initialization + processEarlyPackets() // handle any packets that showed up while we were booting + onNodeDBChanged() } - /// If we just changed our nodedb, we might want to do somethings private fun onNodeDBChanged() { updateNotification() From ab31542fd822c48a036218e47393299c620cb2ea Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 17:36:09 -0700 Subject: [PATCH 31/33] bootstrap the mesh service if someone sets our radio macaddr late --- .../mesh/service/RadioInterfaceService.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index 6a3f4ea46..f834c4a46 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -7,6 +7,7 @@ import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.os.IBinder +import android.os.RemoteException import androidx.core.content.edit import com.geeksville.android.BinaryLogFile import com.geeksville.android.GeeksvilleApplication @@ -177,8 +178,15 @@ class RadioInterfaceService : Service(), Logging { ) // Force the service to reconnect - runningService?.let { - it.setEnabled(addr != null) + val s = runningService + if (s != null) { + info("Setting enable on the running radio service") + s.setEnabled(addr != null) + } else { + if (addr != null) { + info("We have a device addr now, starting mesh service") + MeshService.startService(context) + } } } } @@ -452,15 +460,17 @@ class RadioInterfaceService : Service(), Logging { override fun restartNodeInfo() = doWrite(BTM_NODEINFO_CHARACTER, ByteArray(0)) override fun readMyNode() = - doRead(BTM_MYNODE_CHARACTER) ?: throw Exception("Device returned empty MyNodeInfo") + doRead(BTM_MYNODE_CHARACTER) + ?: throw RemoteException("Device returned empty MyNodeInfo") override fun sendToRadio(a: ByteArray) = handleSendToRadio(a) override fun readRadioConfig() = - doRead(BTM_RADIO_CHARACTER) ?: throw Exception("Device returned empty RadioConfig") + doRead(BTM_RADIO_CHARACTER) + ?: throw RemoteException("Device returned empty RadioConfig") override fun readOwner() = - doRead(BTM_OWNER_CHARACTER) ?: throw Exception("Device returned empty Owner") + doRead(BTM_OWNER_CHARACTER) ?: throw RemoteException("Device returned empty Owner") override fun writeOwner(owner: ByteArray) = doWrite(BTM_OWNER_CHARACTER, owner) From 4f47b619a334ea86f5e0f99f94d84c276c5f7da7 Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 17:37:02 -0700 Subject: [PATCH 32/33] do the painful process of waiting for initial pairing to complete and once it completes automatically connect to the radio should improve user experience for brand new app installs with new devices --- .../com/geeksville/mesh/ui/BTScanScreen.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt index 1811faafc..a239af3ea 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt @@ -3,7 +3,10 @@ package com.geeksville.mesh.ui import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.le.* +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.ParcelUuid import androidx.compose.Composable import androidx.compose.Model @@ -20,6 +23,7 @@ import androidx.ui.material.RadioGroup import androidx.ui.tooling.preview.Preview import com.geeksville.android.Logging import com.geeksville.mesh.service.RadioInterfaceService +import com.geeksville.util.exceptionReporter @Model @@ -185,6 +189,34 @@ fun BTScanScreen() { } else { ScanState.info("Starting bonding for $it") + // We need this receiver to get informed when the bond attempt finished + val bondChangedReceiver = object : BroadcastReceiver() { + + override fun onReceive( + context: Context, + intent: Intent + ) = exceptionReporter { + val state = + intent.getIntExtra( + BluetoothDevice.EXTRA_BOND_STATE, + -1 + ) + ScanState.debug("Received bond state changed $state") + context.unregisterReceiver(this) + if (state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_BONDING) { + ScanState.debug("Bonding completed, connecting service") + ScanUIState.changeSelection( + context, + it.macAddress + ) + } + } + } + + val filter = IntentFilter() + filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) + context.registerReceiver(bondChangedReceiver, filter) + // We ignore missing BT adapters, because it lets us run on the emulator bluetoothAdapter ?.getRemoteDevice(it.macAddress) From d9fe9c6912a98a58ff6fc193a0bd2efd2a80c70c Mon Sep 17 00:00:00 2001 From: geeksville Date: Mon, 30 Mar 2020 17:37:58 -0700 Subject: [PATCH 33/33] release 0.2.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 56ea0e0ba..9d8504258 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 22 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 29 - versionCode 104 - versionName "0.1.4" + versionCode 121 + versionName "0.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes {