Merge pull request #8 from geeksville/master

0.2.1 merge
This commit is contained in:
Kevin Hester 2020-03-30 17:55:40 -07:00 committed by GitHub
commit 63f991d312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 820 additions and 266 deletions

2
.idea/compiler.xml generated
View file

@ -5,6 +5,8 @@
<module name="app" target="1.8" />
<module name="geeksville-androidlib" target="1.7" />
<module name="Mesh Util-geeksville-androidlib" target="1.7" />
<module name="Mesh_Util.app" target="1.8" />
<module name="Mesh_Util.geeksville-androidlib" target="1.7" />
</bytecodeTargetLevel>
</component>
</project>

1
.idea/gradle.xml generated
View file

@ -16,6 +16,7 @@
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>

View file

@ -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

59
TODO.md
View file

@ -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

View file

@ -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")

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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))
}
}

View file

@ -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()
}

View file

@ -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())
}
}

View file

@ -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<Int, NodeInfo>()
@ -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<MeshPacket>()
/// 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()

View file

@ -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<Unit>) {
// 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)

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}
//}

View file

@ -75,7 +75,7 @@ fun MessagesContent() {
val topPad = 4.dp
VerticalScroller(
modifier = LayoutFlexible(1f)
modifier = LayoutWeight(1f)
) {
Column {
messages.forEach { msg ->

View file

@ -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

View file

@ -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")
}

View file

@ -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(

View file

@ -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)
}
// }
}

@ -1 +1 @@
Subproject commit f309ee8f9e9db37daabd7c76da683e052ef62f7a
Subproject commit 1b2449b50d11f66d90511559e94cdf40f525fafb

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,20h12L18,10L6,10v10zM12,13c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L9,8L9,6zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,20h12L18,10L6,10v10zM12,13c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h2c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M5,18.31l3,-1.16L8,5.45L5,6.46zM16,18.55l3,-1.01L19,5.69l-3,1.17z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48L3,20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48L21,3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM8,17.15l-3,1.16L5,6.46l3,-1.01v11.7zM14,18.53l-4,-1.4L10,5.47l4,1.4v11.66zM19,17.54l-3,1.01L16,6.86l3,-1.16v11.84z"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9.83,18l0.59,0.59L12,20.17l1.59,-1.59 0.58,-0.58H19V4H5v14h4.83zM12,5c1.65,0 3,1.35 3,3s-1.35,3 -3,3 -3,-1.35 -3,-3 1.35,-3 3,-3zM6,15.58C6,13.08 9.97,12 12,12s6,1.08 6,3.58V17H6v-1.42z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M9,20l3,3 3,-3h4c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2L5,2c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h4zM5,4h14v14h-4.83l-0.59,0.59L12,20.17l-1.59,-1.59 -0.58,-0.58L5,18L5,4zM12,11c1.65,0 3,-1.35 3,-3s-1.35,-3 -3,-3 -3,1.35 -3,3 1.35,3 3,3zM12,7c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM18,15.58c0,-2.5 -3.97,-3.58 -6,-3.58s-6,1.08 -6,3.58L6,17h12v-1.42zM8.48,15c0.74,-0.51 2.23,-1 3.52,-1s2.78,0.49 3.52,1L8.48,15z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<com.mapbox.mapboxsdk.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mapbox="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
mapbox:mapbox_uiZoomGestures="true"
mapbox:mapbox_uiScrollGestures="true"></com.mapbox.mapboxsdk.maps.MapView>

View file

@ -0,0 +1 @@
../../../../../../mapbox-token.xml

View file

@ -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

@ -1 +1 @@
Subproject commit 188cf4fbb503ac0384f1fce4d3d3f0c2c9f07c02
Subproject commit 65f39f90ce365263620d5f9cbddca0c8abebcf9a

View file

@ -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