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/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
diff --git a/TODO.md b/TODO.md
index b70ec4a89..b7371a95b 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
@@ -28,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
@@ -71,7 +72,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
@@ -161,53 +161,8 @@ 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
-
-
-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...
+* update play store listing for public beta
* track radio brands/regions as a user property (include no-radio as an option)
+* remove mixpanel analytics
+* at connect we might receive messages before finished downloading the nodeinfo. In that case, process those messages later
+
diff --git a/app/build.gradle b/app/build.gradle
index 1ea642841..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 {
@@ -45,7 +45,7 @@ android {
composeOptions {
kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129"
- kotlinCompilerExtensionVersion "0.1.0-dev06"
+ kotlinCompilerExtensionVersion "0.1.0-dev07"
}
}
@@ -88,6 +88,9 @@ dependencies {
//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")
implementation("androidx.ui:ui-graphics:$compose_version")
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/androidx/ui/fakeandroidview/ComposedView.kt b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt
new file mode 100644
index 000000000..0c0e7e4aa
--- /dev/null
+++ b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.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
+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)
+ }
+ }
+
+ init {
+ isClickable = true
+ }
+
+ 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)
+ }
+
+ /**
+ * 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/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index dd816aca1..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.
@@ -200,9 +201,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 +219,10 @@ class MainActivity : AppCompatActivity(), Logging,
// Handle any intent
handleIntent(intent)
+
+ setContent {
+ MeshApp()
+ }
}
override fun onNewIntent(intent: Intent) {
@@ -233,9 +235,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/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
index a30d7ee0e..c7e570bdb 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
@@ -6,9 +6,10 @@ 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") {
+class MeshUtilApplication : GeeksvilleApplication() {
override fun onCreate() {
super.onCreate()
@@ -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/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 14c64d2e0..9e509046e 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -2,18 +2,16 @@ package com.geeksville.mesh.model
import android.content.Context
import android.content.SharedPreferences
-import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
import android.os.RemoteException
-import android.util.Base64
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.ui.getInitials
-import com.google.zxing.BarcodeFormat
-import com.google.zxing.MultiFormatWriter
-import com.journeyapps.barcodescanner.BarcodeEncoder
/// FIXME - figure out how to merge this staate with the AppStatus Model
object UIState : Logging {
@@ -34,32 +32,23 @@ object UIState : Logging {
/// our activity will read this from prefs or set it to the empty string
var ownerName: String = "MrInIDE Ownername"
- /// 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)
+ /// If the app was launched because we received a new channel intent, the Url will be here
+ var requestedChannelUrl: Uri? = null
- return "https://www.meshtastic.org/c/$enc"
- } else {
- return getPreferences(context).getString(
- "owner",
- "https://www.meshtastic.org/c/unset"
- )!!
- }
- }
+ var savedInstanceState: Bundle? = null
- fun getChannelQR(context: Context): Bitmap
- {
- val multiFormatWriter = MultiFormatWriter()
+ /**
+ * 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) }
- val bitMatrix =
- multiFormatWriter.encode(getChannelUrl(context), BarcodeFormat.QR_CODE, 192, 192);
- val barcodeEncoder = BarcodeEncoder()
- return barcodeEncoder.createBitmap(bitMatrix)
+ return if (channel == null && isEmulator)
+ Channel.emulated
+ else
+ channel
}
fun getPreferences(context: Context): SharedPreferences =
@@ -70,7 +59,7 @@ object UIState : Logging {
radioConfig.value = c
getPreferences(context).edit(commit = true) {
- this.putString("channel-url", getChannelUrl(context))
+ this.putString("channel-url", getChannel()!!.getChannelUrl().toString())
}
}
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..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()
@@ -621,9 +642,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()
@@ -663,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()
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..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,12 +7,16 @@ 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
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.*
@@ -168,8 +172,21 @@ class RadioInterfaceService : Service(), Logging {
putString(DEVADDR_KEY, addr)
}
- runningService?.let {
- it.setEnabled(addr != null)
+ // Record that this use has configured a radio
+ GeeksvilleApplication.analytics.track(
+ "mesh_bond"
+ )
+
+ // Force the service to reconnect
+ 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)
+ }
}
}
}
@@ -261,12 +278,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?
@@ -427,13 +459,18 @@ 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 RemoteException("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 RemoteException("Device returned empty RadioConfig")
- override fun readOwner() = doRead(BTM_OWNER_CHARACTER)!!
+ override fun readOwner() =
+ doRead(BTM_OWNER_CHARACTER) ?: throw RemoteException("Device returned empty Owner")
override fun writeOwner(owner: ByteArray) = doWrite(BTM_OWNER_CHARACTER, owner)
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..85daaf0c8
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt
@@ -0,0 +1,88 @@
+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.asModifier
+import androidx.ui.foundation.Box
+import androidx.ui.graphics.*
+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: ImageAsset,
+ modifier: Modifier = Modifier.None,
+ tint: Color? = null
+) {
+ with(DensityAmbient.current) {
+ val imageModifier = ImagePainter(image).asModifier(
+ scaleFit = ScaleFit.FillMaxDimension,
+ colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) }
+ )
+ Box(modifier + ClipModifier + imageModifier)
+ }
+}
+
+
+/// Borrowed from Compose
+class AndroidImage(val bitmap: Bitmap) : ImageAsset {
+
+ /**
+ * @see Image.width
+ */
+ override val width: Int
+ get() = bitmap.width
+
+ /**
+ * @see Image.height
+ */
+ override val height: Int
+ get() = bitmap.height
+
+ override val config: ImageAssetConfig get() = ImageAssetConfig.Argb8888
+
+ /**
+ * @see Image.colorSpace
+ */
+ override val colorSpace: androidx.ui.graphics.colorspace.ColorSpace
+ get() = ColorSpaces.Srgb
+
+ /**
+ * @see Image.hasAlpha
+ */
+ override val hasAlpha: Boolean
+ get() = bitmap.hasAlpha()
+
+ /**
+ * @see Image.nativeImage
+ */
+ override val nativeImage: NativeImageAsset
+ get() = bitmap
+
+ /**
+ * @see
+ */
+ override fun prepareToDraw() {
+ bitmap.prepareToDraw()
+ }
+}
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..c6a9694b8 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
@@ -47,6 +47,7 @@ fun AppDrawer(
ScreenButton(Screen.messages)
ScreenButton(Screen.users)
+ ScreenButton(Screen.map) // turn off for now
ScreenButton(Screen.channel)
ScreenButton(Screen.settings)
}
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..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
@@ -14,12 +17,13 @@ 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
import com.geeksville.android.Logging
import com.geeksville.mesh.service.RadioInterfaceService
+import com.geeksville.util.exceptionReporter
@Model
@@ -89,11 +93,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
@@ -173,7 +179,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 = {
@@ -183,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)
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..0bcad65ff 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,120 @@
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.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.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.UIState
+import com.geeksville.mesh.model.Channel
+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
- )
+ 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.Done,
+ onImeActionPerformed = {
+ TODO()
+ }
+ )
+ } else {
+ Text(
+ text = channel.name,
+ style = typography.h4
+ )
+ }
+ }
- Row(modifier = LayoutGravity.Center) {
// simulated qr code
// val image = imageResource(id = R.drawable.qrcode)
- val image = AndroidImage(UIState.getChannelQR(context))
+ val image = AndroidImage(channel.getChannelQR())
ScaledImage(
image = image,
modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
)
- 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
+ Text(
+ text = "Mode: ${channel.modemConfig.toHumanString()}",
+ modifier = LayoutGravity.Center + LayoutPadding(bottom = 16.dp)
+ )
- 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"
- }
+ Row(modifier = LayoutGravity.Center) {
- val shareIntent = Intent.createChooser(sendIntent, null)
- context.startActivity(shareIntent)
- }) {
- VectorImage(
- id = R.drawable.ic_twotone_share_24,
- tint = palette.onBackground
- )
+ 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
+ )
}
+
+ // Only show the share buttone once we are locked
+ if (!channel.editable)
+ OutlinedButton(modifier = 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, channel.getChannelUrl().toString())
+ 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
- )
}
}
@@ -168,6 +124,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/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt
new file mode 100644
index 000000000..19a721a41
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/Map.kt
@@ -0,0 +1,170 @@
+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
+import androidx.ui.core.ContextAmbient
+import androidx.ui.fakeandroidview.AndroidView
+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.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.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
+
+
+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")
+
+ 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
+ }
+ }
+
+ // 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)) {
+ val f = Feature.fromGeometry(
+ Point.fromLngLat(
+ p.longitude,
+ p.latitude
+ )
+ )
+ 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))
+
+ // 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).withProperties(
+ PropertyFactory.iconImage(markerImageId),
+ PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM)
+ )
+
+ 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)
+
+ mapLifecycleCallbacks.view = view
+ (context.applicationContext as Application).registerActivityLifecycleCallbacks(
+ mapLifecycleCallbacks
+ )
+
+ view.getMapAsync { map ->
+ map.setStyle(Style.OUTDOORS) { style ->
+ 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(9.0).build()
+ map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPos), 1000)
+ }
+ }
+ }
+}
+
+
+@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..b0313b1e0 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
@@ -127,27 +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()
- else -> TODO()
}
+ )
+ }) {
+ when (AppStatus.currentScreen) {
+ Screen.messages -> MessagesContent()
+ Screen.settings -> SettingsContent()
+ Screen.users -> HomeContent()
+ Screen.channel -> ChannelContent(UIState.getChannel())
+ Screen.map -> MapContent()
+ else -> TODO()
}
}
//}
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/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/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/app/src/main/proto b/app/src/main/proto
index f309ee8f9..1b2449b50 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit f309ee8f9e9db37daabd7c76da683e052ef62f7a
+Subproject commit 1b2449b50d11f66d90511559e94cdf40f525fafb
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 @@
+
+
+
+
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/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 @@
+
+
+
+
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..a77c3ecf9
--- /dev/null
+++ b/app/src/main/res/layout/map_view.xml
@@ -0,0 +1,10 @@
+
+
+
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/build.gradle b/build.gradle
index c40721560..1af1d03f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,14 +2,14 @@
buildscript {
ext.kotlin_version = '1.3.61'
- ext.compose_version = '0.1.0-dev06'
+ ext.compose_version = '0.1.0-dev07'
repositories {
google()
jcenter()
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 188cf4fbb..65f39f90c 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit 188cf4fbb503ac0384f1fce4d3d3f0c2c9f07c02
+Subproject commit 65f39f90ce365263620d5f9cbddca0c8abebcf9a
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index be5f8b888..d3c6a75e0 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 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.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-all.zip