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