mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
commit
63f991d312
35 changed files with 820 additions and 266 deletions
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
|
|
@ -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
1
.idea/gradle.xml
generated
|
|
@ -16,6 +16,7 @@
|
|||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -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
59
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
22
app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt
Normal file
22
app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt
Normal file
104
app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
79
app/src/main/java/com/geeksville/mesh/model/Channel.kt
Normal file
79
app/src/main/java/com/geeksville/mesh/model/Channel.kt
Normal 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()
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
88
app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt
Normal file
88
app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
170
app/src/main/java/com/geeksville/mesh/ui/Map.kt
Normal file
170
app/src/main/java/com/geeksville/mesh/ui/Map.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ fun MessagesContent() {
|
|||
val topPad = 4.dp
|
||||
|
||||
VerticalScroller(
|
||||
modifier = LayoutFlexible(1f)
|
||||
modifier = LayoutWeight(1f)
|
||||
) {
|
||||
Column {
|
||||
messages.forEach { msg ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
15
app/src/main/res/drawable/ic_twotone_lock_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_lock_24.xml
Normal 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>
|
||||
15
app/src/main/res/drawable/ic_twotone_lock_open_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_lock_open_24.xml
Normal 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>
|
||||
15
app/src/main/res/drawable/ic_twotone_map_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_map_24.xml
Normal 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>
|
||||
15
app/src/main/res/drawable/ic_twotone_person_pin_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_person_pin_24.xml
Normal 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>
|
||||
10
app/src/main/res/layout/map_view.xml
Normal file
10
app/src/main/res/layout/map_view.xml
Normal 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>
|
||||
1
app/src/main/res/values/mapbox-token.xml
Symbolic link
1
app/src/main/res/values/mapbox-token.xml
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../mapbox-token.xml
|
||||
|
|
@ -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
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue