diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index dba811c87..54874b2cf 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -16,7 +16,6 @@
-
diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml
new file mode 100644
index 000000000..dde5d2b09
--- /dev/null
+++ b/.idea/render.experimental.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 3ae0a59a7..25dbdee10 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -29,7 +29,7 @@ android {
buildFeatures {
// Enables Jetpack Compose for this module
- compose true // NOTE, if true main app crashes if you use regular view layout functions
+ // compose true // NOTE, if true main app crashes if you use regular view layout functions
}
// Set both the Java and Kotlin compilers to target Java 8.
@@ -44,8 +44,8 @@ android {
}
composeOptions {
- kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129"
- kotlinCompilerExtensionVersion "$compose_version"
+ //kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129"
+ //kotlinCompilerExtensionVersion "$compose_version"
}
}
@@ -73,11 +73,17 @@ protobuf {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'com.google.android.material:material:1.0.0'
+ implementation "androidx.fragment:fragment-ktx:1.2.4"
+ implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.viewpager2:viewpager2:1.0.0'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@@ -95,17 +101,6 @@ dependencies {
// 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")
- implementation("androidx.ui:ui-layout:$compose_version")
- implementation("androidx.ui:ui-material:$compose_version")
- implementation("androidx.ui:ui-unit:$compose_version")
- implementation("androidx.ui:ui-util:$compose_version")
- implementation "androidx.ui:ui-tooling:$compose_version"
- androidTestImplementation("androidx.ui:ui-platform:$compose_version")
- androidTestImplementation("androidx.ui:ui-test:$compose_version")
-
// location services
implementation 'com.google.android.gms:play-services-location:17.0.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 70757f77d..aab32d86a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,7 +34,9 @@
-
+
diff --git a/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt b/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt
deleted file mode 100644
index 0c0e7e4aa..000000000
--- a/app/src/main/java/androidx/ui/fakeandroidview/ComposedView.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ui.fakeandroidview
-
-import android.content.Context
-import android.view.LayoutInflater
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import androidx.annotation.LayoutRes
-import androidx.compose.Composable
-
-/**
- * Composes an Android [View] given a layout resource [resId]. The method handles the inflation
- * of the [View] and will call the [postInflationCallback] after this happens. Note that the
- * callback will always be invoked on the main thread.
- *
- * @param resId The id of the layout resource to be inflated.
- * @param postInflationCallback The callback to be invoked after the layout is inflated.
- */
-@Composable
-// TODO(popam): support modifiers here
-fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) {
- AndroidViewHolder(postInflationCallback = postInflationCallback, resId = resId)
-}
-
-
-private class AndroidViewHolder(context: Context) : ViewGroup(context) {
- var view: View? = null
- set(value) {
- if (value != field) {
- field = value
- removeAllViews()
- addView(view)
- }
- }
-
- var postInflationCallback: (View) -> Unit = {}
-
- var resId: Int? = null
- set(value) {
- if (value != field) {
- field = value
- val inflater = LayoutInflater.from(context)
- val view = inflater.inflate(resId!!, this, false)
- this.view = view
- postInflationCallback(view)
- }
- }
-
- init {
- isClickable = true
- }
-
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- view?.measure(widthMeasureSpec, heightMeasureSpec)
- setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
- }
-
- override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
- view?.layout(l, t, r, b)
- }
-
- override fun getLayoutParams(): LayoutParams? {
- return view?.layoutParams ?: LayoutParams(MATCH_PARENT, MATCH_PARENT)
- }
-
- /**
- * Implement this method to handle touch screen motion events.
- *
- *
- * If this method is used to detect click actions, it is recommended that
- * the actions be performed by implementing and calling
- * [.performClick]. This will ensure consistent system behavior,
- * including:
- *
- * * obeying click sound preferences
- * * dispatching OnClickListener calls
- * * handling [ACTION_CLICK][AccessibilityNodeInfo.ACTION_CLICK] when
- * accessibility features are enabled
- *
- *
- * @param event The motion event.
- * @return True if the event was handled, false otherwise.
- */
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- return super.onTouchEvent(event)
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 549f69de2..7fb09f22b 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -1,5 +1,6 @@
package com.geeksville.mesh
+// import kotlinx.android.synthetic.main.tabs.*
import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
@@ -15,29 +16,28 @@ import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.widget.Toast
+import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
-import androidx.ui.core.setContent
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
-import com.geeksville.mesh.model.MessagesState
-import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.TextMessage
-import com.geeksville.mesh.model.UIState
+import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.*
-import com.geeksville.mesh.ui.AppStatus
-import com.geeksville.mesh.ui.MeshApp
-import com.geeksville.mesh.ui.ScanState
-import com.geeksville.mesh.ui.Screen
+import com.geeksville.mesh.ui.*
import com.geeksville.util.Exceptions
import com.geeksville.util.exceptionReporter
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.tasks.Task
+import com.google.android.material.tabs.TabLayoutMediator
+import kotlinx.android.synthetic.main.activity_main.*
import java.nio.charset.Charset
-
/*
UI design
@@ -105,6 +105,55 @@ class MainActivity : AppCompatActivity(), Logging,
bluetoothManager.adapter
}
+ private val model: UIViewModel by viewModels()
+
+ data class TabInfo(val text: String, val icon: Int, val content: Fragment)
+
+ // private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings
+ private val tabInfos = arrayOf(
+ TabInfo(
+ "Messages",
+ R.drawable.ic_twotone_message_24,
+ MessagesFragment()
+ ),
+ TabInfo(
+ "Users",
+ R.drawable.ic_twotone_people_24,
+ UsersFragment()
+ ),
+ TabInfo(
+ "Map",
+ R.drawable.ic_twotone_map_24,
+ MapFragment()
+ ),
+ TabInfo(
+ "Channel",
+ R.drawable.ic_twotone_contactless_24,
+ ChannelFragment()
+ ),
+ TabInfo(
+ "Settings",
+ R.drawable.ic_twotone_settings_applications_24,
+ SettingsFragment()
+ )
+ )
+
+ private
+ val tabsAdapter = object : FragmentStateAdapter(this) {
+
+ override fun getItemCount(): Int = tabInfos.size
+
+ override fun createFragment(position: Int): Fragment {
+ // Return a NEW fragment instance in createFragment(int)
+ /*
+ fragment.arguments = Bundle().apply {
+ // Our object is just an integer :-P
+ putInt(ARG_OBJECT, position + 1)
+ } */
+ return tabInfos[position].content
+ }
+ }
+
private fun requestPermission() {
debug("Checking permissions")
@@ -139,7 +188,11 @@ class MainActivity : AppCompatActivity(), Logging,
}
// Ask for all the missing perms
- ActivityCompat.requestPermissions(this, missingPerms.toTypedArray(), DID_REQUEST_PERM)
+ ActivityCompat.requestPermissions(
+ this,
+ missingPerms.toTypedArray(),
+ DID_REQUEST_PERM
+ )
// DID_REQUEST_PERM is an
// app-defined int constant. The callback method gets the
@@ -161,7 +214,7 @@ class MainActivity : AppCompatActivity(), Logging,
private fun sendTestPackets() {
exceptionReporter {
- val m = UIState.meshService!!
+ val m = model.meshService!!
// Do some test operations
val testPayload = "hello world".toByteArray()
@@ -182,10 +235,8 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val prefs = UIState.getPreferences(this)
- UIState.ownerName = prefs.getString("owner", "")!!
- UIState.meshService = null
- UIState.savedInstanceState = savedInstanceState
+ val prefs = UIViewModel.getPreferences(this)
+ model.ownerName.value = prefs.getString("owner", "")!!
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
@@ -195,7 +246,11 @@ class MainActivity : AppCompatActivity(), Logging,
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
} else {
- Toast.makeText(this, "Error - this app requires bluetooth", Toast.LENGTH_LONG)
+ Toast.makeText(
+ this,
+ "Error - this app requires bluetooth",
+ Toast.LENGTH_LONG
+ )
.show()
}
@@ -220,11 +275,32 @@ class MainActivity : AppCompatActivity(), Logging,
// Handle any intent
handleIntent(intent)
- setContent {
+ /* setContent {
MeshApp()
- }
+ } */
+ setContentView(R.layout.activity_main)
+
+ pager.adapter = tabsAdapter
+ pager.isUserInputEnabled =
+ false // Gestures for screen switching doesn't work so good with the map view
+ // pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops
+ TabLayoutMediator(tab_layout, pager) { tab, position ->
+ // tab.text = tabInfos[position].text // I think it looks better with icons only
+ tab.icon = getDrawable(tabInfos[position].icon)
+ }.attach()
+
+ model.isConnected.observe(this, Observer { connected ->
+ val image = when (connected) {
+ MeshService.ConnectionState.CONNECTED -> R.drawable.cloud_on
+ MeshService.ConnectionState.DEVICE_SLEEP -> R.drawable.ic_twotone_cloud_upload_24
+ MeshService.ConnectionState.DISCONNECTED -> R.drawable.cloud_off
+ }
+
+ connectStatusImage.setImageDrawable(getDrawable(image))
+ })
}
+
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
@@ -235,26 +311,26 @@ 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) {
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
+ val requestedChannelUrl = appLinkData
}
}
override fun onDestroy() {
unregisterMeshReceiver()
- UIState.meshService =
- null // When our activity goes away make sure we don't keep a ptr around to the service
super.onDestroy()
}
/**
* Dispatch incoming result to the correct fragment.
*/
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ override fun onActivityResult(
+ requestCode: Int,
+ resultCode: Int,
+ data: Intent?
+ ) {
super.onActivityResult(requestCode, resultCode, data)
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
@@ -281,7 +357,8 @@ class MainActivity : AppCompatActivity(), Logging,
} */
}
- private var receiverRegistered = false
+ private
+ var receiverRegistered = false
private fun registerMeshReceiver() {
logAssert(!receiverRegistered)
@@ -300,40 +377,27 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
- /// Read the config bytes from the radio so we can show them in our GUI, the radio's copy is ground truth
- private fun readRadioConfig() {
- val bytes = UIState.meshService!!.radioConfig
-
- val config = MeshProtos.RadioConfig.parseFrom(bytes)
- UIState.setRadioConfig(this, config)
-
- debug("Read config from radio")
- }
/// Called when we gain/lose a connection to our mesh radio
private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) {
- UIState.isConnected.value = connected
- debug("connchange ${UIState.isConnected.value}")
+ model.isConnected.value = connected
+ debug("connchange ${model.isConnected.value}")
if (connected == MeshService.ConnectionState.CONNECTED) {
- // always get the current radio config when we connect
- readRadioConfig()
// everytime the radio reconnects, we slam in our current owner data, the radio is smart enough to only broadcast if needed
- UIState.setOwner(this)
+ model.setOwner(this)
- val m = UIState.meshService!!
+ val m = model.meshService!!
// Pull down our real node ID
- NodeDB.myId.value = m.myId
+ model.nodeDB.myId.value = m.myId
// Update our nodeinfos based on data from the device
- NodeDB.nodes.clear()
- NodeDB.nodes.putAll(
- m.nodes.map
- {
- it.user?.id!! to it
- }
- )
+ val nodes = m.nodes.map {
+ it.user?.id!! to it
+ }.toMap()
+
+ model.nodeDB.nodes.value = nodes
}
}
@@ -349,76 +413,95 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
- private val meshServiceReceiver = object : BroadcastReceiver() {
+ private
+ val meshServiceReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
- debug("Received from mesh service $intent")
+ override fun onReceive(context: Context, intent: Intent) =
+ exceptionReporter {
+ debug("Received from mesh service $intent")
- when (intent.action) {
- MeshService.ACTION_NODE_CHANGE -> {
- val info: NodeInfo = intent.getParcelableExtra(EXTRA_NODEINFO)!!
- debug("UI nodechange $info")
+ when (intent.action) {
+ MeshService.ACTION_NODE_CHANGE -> {
+ val info: NodeInfo =
+ intent.getParcelableExtra(EXTRA_NODEINFO)!!
+ debug("UI nodechange $info")
- // We only care about nodes that have user info
- info.user?.id?.let {
- NodeDB.nodes[it] = info
- }
- }
-
- MeshService.ACTION_RECEIVED_DATA -> {
- debug("TODO rxdata")
- val sender = intent.getStringExtra(EXTRA_SENDER)!!
- val payload = intent.getByteArrayExtra(EXTRA_PAYLOAD)!!
- val typ = intent.getIntExtra(EXTRA_TYP, -1)
-
- when (typ) {
- MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
- // FIXME - use the real time from the packet
- // FIXME - don't just slam in a new list each time, it probably causes extra drawing. Figure out how to be Compose smarter...
- val msg = TextMessage(sender, payload.toString(utf8))
-
- MessagesState.addMessage(msg)
+ // We only care about nodes that have user info
+ info.user?.id?.let {
+ val newnodes = model.nodeDB.nodes.value!! + Pair(it, info)
+ model.nodeDB.nodes.value = newnodes
}
- else -> TODO()
}
+
+ MeshService.ACTION_RECEIVED_DATA -> {
+ debug("TODO rxdata")
+ val sender =
+ intent.getStringExtra(EXTRA_SENDER)!!
+ val payload =
+ intent.getByteArrayExtra(EXTRA_PAYLOAD)!!
+ val typ = intent.getIntExtra(EXTRA_TYP, -1)
+
+ when (typ) {
+ MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
+ // FIXME - use the real time from the packet
+ // FIXME - don't just slam in a new list each time, it probably causes extra drawing. Figure out how to be Compose smarter...
+ val msg = TextMessage(
+ sender,
+ payload.toString(utf8)
+ )
+
+ model.messagesState.addMessage(msg)
+ }
+ else -> TODO()
+ }
+ }
+ MeshService.ACTION_MESH_CONNECTED -> {
+ val connected =
+ MeshService.ConnectionState.valueOf(
+ intent.getStringExtra(
+ EXTRA_CONNECTED
+ )!!
+ )
+ onMeshConnectionChanged(connected)
+ }
+ else -> TODO()
}
- MeshService.ACTION_MESH_CONNECTED -> {
- val connected =
- MeshService.ConnectionState.valueOf(intent.getStringExtra(EXTRA_CONNECTED)!!)
- onMeshConnectionChanged(connected)
- }
- else -> TODO()
}
- }
}
- private val mesh = object : ServiceClient({
- com.geeksville.mesh.IMeshService.Stub.asInterface(it)
- }) {
- override fun onConnected(service: com.geeksville.mesh.IMeshService) {
- UIState.meshService = service
+ private
+ val mesh = object :
+ ServiceClient({
+ com.geeksville.mesh.IMeshService.Stub.asInterface(it)
+ }) {
+ override fun onConnected(service: com.geeksville.mesh.IMeshService) = exceptionReporter {
+ model.meshService = service
+
+ debug("Getting latest radioconfig from service")
+ model.radioConfig.value = MeshProtos.RadioConfig.parseFrom(service.radioConfig)
// We don't start listening for packets until after we are connected to the service
registerMeshReceiver()
// We won't receive a notify for the initial state of connection, so we force an update here
- val connectionState = MeshService.ConnectionState.valueOf(service.connectionState())
+ val connectionState =
+ MeshService.ConnectionState.valueOf(service.connectionState())
onMeshConnectionChanged(connectionState)
- debug("connected to mesh service, isConnected=${UIState.isConnected.value}")
+ debug("connected to mesh service, isConnected=${model.isConnected.value}")
}
override fun onDisconnected() {
unregisterMeshReceiver()
- UIState.meshService = null
+ model.meshService = null
}
}
private fun bindMeshService() {
debug("Binding to mesh service!")
// we bind using the well known name, to make sure 3rd party apps could also
- if (UIState.meshService != null)
+ if (model.meshService != null)
Exceptions.reportError("meshService was supposed to be null, ignoring (but reporting a bug)")
MeshService.startService(this)?.let { intent ->
@@ -433,11 +516,10 @@ class MainActivity : AppCompatActivity(), Logging,
// if we never connected, do nothing
debug("Unbinding from mesh service!")
mesh.close()
- UIState.meshService = null
+ model.meshService = null
}
override fun onStop() {
- ScanState.stopScan()
unregisterMeshReceiver() // No point in receiving updates while the GUI is gone, we'll get them when the user launches the activity
unbindMeshService()
@@ -449,9 +531,12 @@ class MainActivity : AppCompatActivity(), Logging,
bindMeshService()
- val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
+ val bonded =
+ RadioInterfaceService.getBondedDeviceAddress(this) != null
+ /* FIXME - not yet working
if (!bonded)
AppStatus.currentScreen = Screen.settings
+ */
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
index ce3e1cc5f..d3c0a6890 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
@@ -3,7 +3,6 @@ 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
@@ -11,7 +10,6 @@ import com.journeyapps.barcodescanner.BarcodeEncoder
import java.net.MalformedURLException
-@Model
data class Channel(
var name: String,
var modemConfig: MeshProtos.ChannelSettings.ModemConfig,
diff --git a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt
index c42c5ce01..3b1f1ede8 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt
@@ -1,7 +1,7 @@
package com.geeksville.mesh.model
import android.os.RemoteException
-import androidx.compose.frames.modelListOf
+import androidx.lifecycle.MutableLiveData
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.android.Logging
import com.geeksville.mesh.MeshProtos
@@ -21,8 +21,8 @@ data class TextMessage(
)
-object MessagesState : Logging {
- private val testTexts = arrayOf(
+class MessagesState(private val ui: UIViewModel) : Logging {
+ private val testTexts = listOf(
TextMessage(
"+16508765310",
"I found the cache"
@@ -35,17 +35,20 @@ object MessagesState : Logging {
// If the following (unused otherwise) line is commented out, the IDE preview window works.
// if left in the preview always renders as empty.
- val messages = modelListOf(* if (isEmulator) testTexts else arrayOf())
+ val messages =
+ object : MutableLiveData>(if (isEmulator) testTexts else listOf()) {
+
+ }
/// add a message our GUI list of past msgs
fun addMessage(m: TextMessage) {
- messages.add(m)
+ messages.value = messages.value!! + m
}
/// Send a message and added it to our GUI log
fun sendMessage(str: String, dest: String? = null) {
var error: String? = null
- val service = UIState.meshService
+ val service = ui.meshService
if (service != null)
try {
service.sendData(
@@ -59,9 +62,9 @@ object MessagesState : Logging {
else
error = "Error: No Mesh service"
- MessagesState.addMessage(
+ addMessage(
TextMessage(
- NodeDB.myId.value,
+ ui.nodeDB.myId.value!!,
str,
errorMessage = error
)
diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt
index 5802cf4b3..6313d5a8f 100644
--- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt
@@ -1,13 +1,14 @@
package com.geeksville.mesh.model
-import androidx.compose.frames.modelMapOf
-import androidx.compose.mutableStateOf
+import androidx.lifecycle.MutableLiveData
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
-object NodeDB {
+
+/// NodeDB lives inside the UIViewModel, but it needs a backpointer to reach the service
+class NodeDB(private val ui: UIViewModel) {
private val testPositions = arrayOf(
Position(32.776665, -96.796989, 35), // dallas
Position(32.960758, -96.733521, 35), // richardson
@@ -43,12 +44,14 @@ object NodeDB {
private val seedWithTestNodes = isEmulator
/// The unique ID of our node
- val myId = mutableStateOf(if (isEmulator) "+16508765309" else "invalid")
+ val myId = object : MutableLiveData(if (isEmulator) "+16508765309" else "invalid") {}
/// A map from nodeid to to nodeinfo
val nodes =
- modelMapOf(* (if (isEmulator) testNodes else listOf()).map { it.user!!.id to it }.toTypedArray())
+ object :
+ MutableLiveData>(mapOf(*(if (isEmulator) testNodes else listOf()).map { it.user!!.id to it }
+ .toTypedArray())) {}
/// Could be null if we haven't received our node DB yet
- val ourNodeInfo get() = nodes[myId.value]
+ val ourNodeInfo get() = nodes.value!![myId.value]
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index a3fa1f6d8..1b5e7b435 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -3,72 +3,98 @@ package com.geeksville.mesh.model
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
-import android.os.Bundle
import android.os.RemoteException
-import androidx.compose.mutableStateOf
import androidx.core.content.edit
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
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.service.MeshService
-import com.geeksville.mesh.ui.getInitials
-/// FIXME - figure out how to merge this staate with the AppStatus Model
-object UIState : Logging {
+/// Given a human name, strip out the first letter of the first three words and return that as the initials for
+/// that user.
+fun getInitials(name: String): String {
+ val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }.take(3).map { it.first() }
+ .joinToString("")
+
+ return words
+}
+
+class UIViewModel : ViewModel(), Logging {
+ init {
+ debug("ViewModel created")
+ }
+
+ companion object {
+ /**
+ * 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(c: MeshProtos.RadioConfig?): Channel? {
+ val channel = c?.channelSettings?.let { Channel(it) }
+
+ return if (channel == null && isEmulator)
+ Channel.emulated
+ else
+ channel
+ }
+
+ fun getPreferences(context: Context): SharedPreferences =
+ context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
+ }
+
+ var meshService: IMeshService? = null
+
+ val nodeDB = NodeDB(this)
+ val messagesState = MessagesState(this)
+
+ /// Are we connected to our radio device
+ val isConnected =
+ object :
+ MutableLiveData(MeshService.ConnectionState.DISCONNECTED) {
+ }
+
+ /// various radio settings (including the channel)
+ val radioConfig = object : MutableLiveData(null) {
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ debug("ViewModel cleared")
+ }
+
+ /// Set the radio config (also updates our saved copy in preferences)
+ fun setRadioConfig(context: Context, c: MeshProtos.RadioConfig) {
+ debug("Setting new radio config!")
+ meshService?.radioConfig = c.toByteArray()
+ radioConfig.value = c
+
+ getPreferences(context).edit(commit = true) {
+ this.putString("channel-url", getChannel(c)!!.getChannelUrl().toString())
+ }
+ }
/// Kinda ugly - created in the activity but used from Compose - figure out if there is a cleaner way GIXME
// lateinit var googleSignInClient: GoogleSignInClient
- var meshService: IMeshService? = null
-
- /// Are we connected to our radio device
- val isConnected = mutableStateOf(MeshService.ConnectionState.DISCONNECTED)
-
- /// various radio settings (including the channel)
- private val radioConfig = mutableStateOf(null)
-
/// our name in hte radio
/// Note, we generate owner initials automatically for now
/// our activity will read this from prefs or set it to the empty string
- var ownerName: String = "MrInIDE Ownername"
+ val ownerName = object : MutableLiveData("MrIDE Test") {
+ }
+
/// If the app was launched because we received a new channel intent, the Url will be here
var requestedChannelUrl: Uri? = null
- var savedInstanceState: Bundle? = null
-
- /**
- * Return the current channel info
- * FIXME, we should sim channels at the MeshService level if we are running on an emulator,
- * for now I just fake it by returning a canned channel.
- */
- fun getChannel(): Channel? {
- val channel = radioConfig.value?.channelSettings?.let { Channel(it) }
-
- return if (channel == null && isEmulator)
- Channel.emulated
- else
- channel
- }
-
- fun getPreferences(context: Context): SharedPreferences =
- context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
-
- /// Set the radio config (also updates our saved copy in preferences)
- fun setRadioConfig(context: Context, c: MeshProtos.RadioConfig) {
- radioConfig.value = c
-
- getPreferences(context).edit(commit = true) {
- this.putString("channel-url", getChannel()!!.getChannelUrl().toString())
- }
- }
-
// clean up all this nasty owner state management FIXME
fun setOwner(context: Context, s: String? = null) {
if (s != null) {
- ownerName = s
+ ownerName.value = s
// note: we allow an empty userstring to be written to prefs
getPreferences(context).edit(commit = true) {
@@ -77,15 +103,16 @@ object UIState : Logging {
}
// Note: we are careful to not set a new unique ID
- if (ownerName.isNotEmpty())
+ if (ownerName.value!!.isNotEmpty())
try {
meshService?.setOwner(
null,
- ownerName,
- getInitials(ownerName)
+ ownerName.value,
+ getInitials(ownerName.value!!)
) // Note: we use ?. here because we might be running in the emulator
} catch (ex: RemoteException) {
errormsg("Can't set username on device, is device offline? ${ex.message}")
}
}
}
+
diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
index c4d2ab131..e6dddbc4c 100644
--- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
@@ -316,7 +316,7 @@ class RadioInterfaceService : Service(), Logging {
debug("requested MTU result=$mtuRes")
mtuRes.getOrThrow() // FIXME - why sometimes is the result Unit!?!
- fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)
+ fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)!!
// We must set this to true before broadcasting connectionChanged
isConnected = true
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Analytics.kt b/app/src/main/java/com/geeksville/mesh/ui/Analytics.kt
deleted file mode 100644
index 57fedcb35..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Analytics.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.compose.onCommit
-import com.geeksville.android.GeeksvilleApplication
-
-/**
- * Track compose screen visibility
- */
-@Composable
-fun analyticsScreen(name: String) {
- onCommit(AppStatus.currentScreen) {
- GeeksvilleApplication.analytics.sendScreenView(name)
-
- onDispose {
- GeeksvilleApplication.analytics.endScreenView()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt b/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt
deleted file mode 100644
index 85daaf0c8..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/AndroidImage.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.geeksville.mesh.ui
-
-import android.graphics.Bitmap
-import androidx.compose.Composable
-import androidx.ui.core.DensityAmbient
-import androidx.ui.core.DrawModifier
-import androidx.ui.core.Modifier
-import androidx.ui.core.asModifier
-import androidx.ui.foundation.Box
-import androidx.ui.graphics.*
-import androidx.ui.graphics.colorspace.ColorSpaces
-import androidx.ui.graphics.painter.ImagePainter
-import androidx.ui.unit.Density
-import androidx.ui.unit.PxSize
-import androidx.ui.unit.toRect
-
-/// Stolen from the Compose SimpleImage, replace with their real Image component someday
-// TODO(mount, malkov) : remove when RepaintBoundary is a modifier: b/149982905
-// This is class and not val because if b/149985596
-private object ClipModifier : DrawModifier {
- override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) {
- canvas.save()
- canvas.clipRect(size.toRect())
- drawContent()
- canvas.restore()
- }
-}
-
-
-/// Stolen from the Compose SimpleImage, replace with their real Image component someday
-@Composable
-fun ScaledImage(
- image: ImageAsset,
- modifier: Modifier = Modifier.None,
- tint: Color? = null
-) {
- with(DensityAmbient.current) {
- val imageModifier = ImagePainter(image).asModifier(
- scaleFit = ScaleFit.FillMaxDimension,
- colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) }
- )
- Box(modifier + ClipModifier + imageModifier)
- }
-}
-
-
-/// Borrowed from Compose
-class AndroidImage(val bitmap: Bitmap) : ImageAsset {
-
- /**
- * @see Image.width
- */
- override val width: Int
- get() = bitmap.width
-
- /**
- * @see Image.height
- */
- override val height: Int
- get() = bitmap.height
-
- override val config: ImageAssetConfig get() = ImageAssetConfig.Argb8888
-
- /**
- * @see Image.colorSpace
- */
- override val colorSpace: androidx.ui.graphics.colorspace.ColorSpace
- get() = ColorSpaces.Srgb
-
- /**
- * @see Image.hasAlpha
- */
- override val hasAlpha: Boolean
- get() = bitmap.hasAlpha()
-
- /**
- * @see Image.nativeImage
- */
- override val nativeImage: NativeImageAsset
- get() = bitmap
-
- /**
- * @see
- */
- override fun prepareToDraw() {
- bitmap.prepareToDraw()
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt b/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt
deleted file mode 100644
index 4de02356f..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/AppDrawer.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.annotation.DrawableRes
-import androidx.compose.Composable
-import androidx.ui.core.Modifier
-import androidx.ui.foundation.Text
-import androidx.ui.foundation.shape.corner.RoundedCornerShape
-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.tooling.preview.Preview
-import androidx.ui.unit.dp
-import com.geeksville.mesh.R
-
-
-@Composable
-fun AppDrawer(
- currentScreen: ScreenInfo,
- closeDrawer: () -> Unit
-) {
- Column(modifier = LayoutSize.Fill) {
- Spacer(LayoutHeight(24.dp))
- Row(modifier = LayoutPadding(16.dp)) {
- VectorImage(
- id = R.drawable.ic_launcher_new_foreground,
- tint = MaterialTheme.colors.primary
- )
- Spacer(LayoutWidth(8.dp))
- // VectorImage(id = R.drawable.ic_launcher_new_foreground)
- }
- Divider(color = Color(0x14333333))
-
- @Composable
- fun ScreenButton(screen: ScreenInfo) {
- DrawerButton(
- icon = screen.icon,
- label = screen.label,
- isSelected = currentScreen == screen
- ) {
- navigateTo(screen)
- closeDrawer()
- }
- }
-
- ScreenButton(Screen.messages)
- ScreenButton(Screen.users)
- ScreenButton(Screen.map) // turn off for now
- ScreenButton(Screen.channel)
- ScreenButton(Screen.settings)
- }
-}
-
-@Composable
-private fun DrawerButton(
- modifier: Modifier = Modifier.None,
- @DrawableRes icon: Int,
- label: String,
- isSelected: Boolean,
- action: () -> Unit
-) {
- val colors = MaterialTheme.colors
- val textIconColor = if (isSelected) {
- colors.primary
- } else {
- colors.onSurface.copy(alpha = 0.6f)
- }
- val backgroundColor = if (isSelected) {
- colors.primary.copy(alpha = 0.12f)
- } else {
- colors.surface
- }
-
- Surface(
- modifier = modifier + LayoutPadding(
- start = 8.dp,
- top = 8.dp,
- end = 8.dp,
- bottom = 0.dp
- ),
- color = backgroundColor,
- shape = RoundedCornerShape(4.dp)
- ) {
- TextButton(onClick = action) {
- Row {
- VectorImage(
- modifier = LayoutGravity.Center,
- id = icon,
- tint = textIconColor
- )
- Spacer(LayoutWidth(16.dp))
- Text(
- text = label,
- style = (MaterialTheme.typography).body2.copy(
- color = textIconColor
- ),
- modifier = LayoutWidth.Fill
- )
- }
- }
- }
-}
-
-
-@Preview
-@Composable
-fun previewDrawer() {
- AppDrawer(
- currentScreen = AppStatus.currentScreen,
- closeDrawer = { }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt
deleted file mode 100644
index 0cb0de09b..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-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
-import androidx.compose.frames.modelMapOf
-import androidx.compose.onCommit
-import androidx.ui.core.ContextAmbient
-import androidx.ui.foundation.Text
-import androidx.ui.layout.Column
-import androidx.ui.layout.LayoutGravity
-import androidx.ui.material.CircularProgressIndicator
-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
-object ScanUIState {
- var selectedMacAddr: String? = null
- var errorText: String? = null
-
- val devices = modelMapOf()
-
- /// Change to a new macaddr selection, updating GUI and radio
- fun changeSelection(context: Context, newAddr: String) {
- ScanState.info("Changing BT device to $newAddr")
- selectedMacAddr = newAddr
- RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
- }
-}
-
-/// FIXME, remove once compose has better lifecycle management
-object ScanState : Logging {
- var scanner: BluetoothLeScanner? = null
- var callback: ScanCallback? = null // SUPER NASTY FIXME
-
- fun stopScan() {
- if (callback != null) {
- debug("stopping scan")
- try {
- scanner!!.stopScan(callback)
- } catch (ex: Throwable) {
- warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
- }
- callback = null
- }
- }
-}
-
-@Model
-data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
- val isSelected get() = macAddress == ScanUIState.selectedMacAddr
-}
-
-
-@Composable
-fun BTScanScreen() {
- val context = ContextAmbient.current
-
- /// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
- val bluetoothAdapter =
- (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
-
- analyticsScreen(name = "settings")
- onCommit(AppStatus.currentScreen) {
- ScanState.debug("BTScan component active")
- ScanUIState.selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
-
- val scanCallback = object : ScanCallback() {
- override fun onScanFailed(errorCode: Int) {
- val msg = "Unexpected bluetooth scan failure: $errorCode"
- // error code2 seeems to be indicate hung bluetooth stack
- ScanUIState.errorText = msg
- ScanState.reportError(msg)
- }
-
- // For each device that appears in our scan, ask for its GATT, when the gatt arrives,
- // check if it is an eligable device and store it in our list of candidates
- // if that device later disconnects remove it as a candidate
- override fun onScanResult(callbackType: Int, result: ScanResult) {
-
- val addr = result.device.address
- // prevent logspam because weill get get lots of redundant scan results
- 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,
- isBonded
- )
- ScanState.debug("onScanResult ${entry}")
- ScanUIState.devices[addr] = entry
-
- // If nothing was selected, by default select the first thing we see
- if (ScanUIState.selectedMacAddr == null && entry.bonded)
- ScanUIState.changeSelection(context, addr)
- }
- }
- }
- if (bluetoothAdapter == null) {
- ScanState.warn("No bluetooth adapter. Running under emulation?")
-
- val testnodes = listOf(
- BTScanEntry("Meshtastic_ab12", "xx", false),
- BTScanEntry("Meshtastic_32ac", "xb", true)
- )
-
- ScanUIState.devices.putAll(testnodes.map { it.macAddress to it })
-
- // If nothing was selected, by default select the first thing we see
- if (ScanUIState.selectedMacAddr == null)
- ScanUIState.changeSelection(context, testnodes.first().macAddress)
- } else {
- /// The following call might return null if the user doesn't have bluetooth access permissions
- val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
-
- if (s == null) {
- ScanUIState.errorText =
- "This application requires bluetooth access. Please grant access in android settings."
- } else {
- ScanState.debug("starting scan")
-
- // filter and only accept devices that have a sw update service
- val filter =
- ScanFilter.Builder()
- .setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
- .build()
-
- val settings =
- ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
- s.startScan(listOf(filter), settings, scanCallback)
- ScanState.scanner = s
- ScanState.callback = scanCallback
- }
- }
-
- onDispose {
- ScanState.debug("BTScan component deactivated")
- ScanState.stopScan()
- }
- }
-
- Column {
- if (ScanUIState.errorText != null) {
- Text(text = ScanUIState.errorText!!)
- } else {
- if (ScanUIState.devices.isEmpty()) {
- Text(
- text = "Looking for Meshtastic devices... (zero found)",
- modifier = LayoutGravity.Center
- )
-
- CircularProgressIndicator() // Show that we are searching still
- } else {
- // val allPaired = bluetoothAdapter?.bondedDevices.orEmpty().map { it.address }.toSet()
-
- /* Only let user select paired devices
- val paired = devices.values.filter { allPaired.contains(it.macAddress) }
- if (paired.size < devices.size) {
- Text(
- "Warning: there are nearby Meshtastic devices that are not paired with this phone. Before you can select a device, you will need to pair it in Bluetooth Settings."
- )
- } */
-
- RadioGroup {
- Column {
- ScanUIState.devices.values.forEach {
- // disabled pending https://issuetracker.google.com/issues/149528535
- ProvideEmphasis(emphasis = if (it.bonded) MaterialTheme.emphasisLevels.high else MaterialTheme.emphasisLevels.disabled) {
- RadioGroupTextItem(
- selected = (it.isSelected),
- onSelect = {
- // If the device is paired, let user select it, otherwise start the pairing flow
- if (it.bonded) {
- ScanUIState.changeSelection(context, it.macAddress)
- } 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)
- ?.createBond()
- }
- },
- text = it.name
- )
- }
- }
- }
- }
- }
- }
- }
-}
-
-
-@Preview
-@Composable
-fun btScanScreenPreview() {
- BTScanScreen()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/Channel.kt
deleted file mode 100644
index d8b01d8d6..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-package com.geeksville.mesh.ui
-
-import android.content.Intent
-import androidx.compose.Composable
-import androidx.ui.core.ContextAmbient
-import androidx.ui.foundation.Text
-import androidx.ui.input.ImeAction
-import androidx.ui.layout.*
-import androidx.ui.material.MaterialTheme
-import androidx.ui.material.OutlinedButton
-import androidx.ui.tooling.preview.Preview
-import androidx.ui.unit.dp
-import com.geeksville.analytics.DataPair
-import com.geeksville.android.GeeksvilleApplication
-import com.geeksville.android.Logging
-import com.geeksville.mesh.R
-import com.geeksville.mesh.model.Channel
-import com.geeksville.mesh.model.toHumanString
-
-
-object ChannelLog : Logging
-
-
-@Composable
-fun ChannelContent(channel: Channel?) {
- analyticsScreen(name = "channel")
-
- val typography = MaterialTheme.typography
- val context = ContextAmbient.current
-
- Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
- if (channel != null) {
- Row(modifier = LayoutGravity.Center) {
-
- Text(text = "Channel ", modifier = LayoutGravity.Center)
-
- if (channel.editable) {
- // FIXME - limit to max length
- StyledTextField(
- value = channel.name,
- onValueChange = { channel.name = it },
- textStyle = typography.h4.copy(
- color = palette.onSecondary.copy(alpha = 0.8f)
- ),
- imeAction = ImeAction.Done,
- onImeActionPerformed = {
- ChannelLog.errormsg("FIXME, implement channel edit button")
- }
- )
- } else {
- Text(
- text = channel.name,
- style = typography.h4
- )
- }
- }
-
- // simulated qr code
- // val image = imageResource(id = R.drawable.qrcode)
- val image = AndroidImage(channel.getChannelQR())
-
- ScaledImage(
- image = image,
- modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
- )
-
- Text(
- text = "Mode: ${channel.modemConfig.toHumanString()}",
- modifier = LayoutGravity.Center + LayoutPadding(bottom = 16.dp)
- )
-
- Row(modifier = LayoutGravity.Center) {
-
- 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
- )
- }
- }
- }
- }
-}
-
-
-@Preview
-@Composable
-fun previewChannel() {
- // another bug? It seems modaldrawerlayout not yet supported in preview
- MaterialTheme(colors = palette) {
- ChannelContent(Channel.emulated)
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
new file mode 100644
index 000000000..4d8136f2f
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -0,0 +1,187 @@
+package com.geeksville.mesh.ui
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+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.UIViewModel
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.channel_fragment.*
+
+
+class ChannelFragment : ScreenFragment("Channel"), Logging {
+
+ private val model: UIViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.channel_fragment, container, false)
+ }
+
+ private fun onEditingChanged() {
+ val isEditing = editableCheckbox.isChecked
+
+ channelOptions.isEnabled = false // Not yet ready
+ shareButton.isEnabled = !isEditing
+ channelNameView.isEnabled = isEditing
+ qrView.visibility =
+ if (isEditing) View.INVISIBLE else View.VISIBLE // Don't show the user a stale QR code
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ onEditingChanged() // Set initial state
+
+ editableCheckbox.setOnCheckedChangeListener { _, checked ->
+ onEditingChanged()
+
+ if (!checked) {
+ // User just locked it, we should warn and then apply changes to radio FIXME not ready yet
+ Snackbar.make(
+ editableCheckbox,
+ "Changing channels is not yet supported",
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ model.radioConfig.observe(viewLifecycleOwner, Observer { config ->
+ val channel = UIViewModel.getChannel(config)
+
+ if (channel != null) {
+ qrView.visibility = View.VISIBLE
+ channelNameEdit.visibility = View.VISIBLE
+ channelNameEdit.setText(channel.name)
+ editableCheckbox.isEnabled = true
+
+ qrView.setImageBitmap(channel.getChannelQR())
+ // Share this particular channel if someone clicks share
+ shareButton.setOnClickListener {
+ 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)
+ requireActivity().startActivity(shareIntent)
+ }
+ } else {
+ qrView.visibility = View.INVISIBLE
+ channelNameEdit.visibility = View.INVISIBLE
+ editableCheckbox.isEnabled = false
+ }
+
+ val adapter = ArrayAdapter(
+ requireContext(),
+ R.layout.dropdown_menu_popup_item,
+ arrayOf("Item 1", "Item 2", "Item 3", "Item 4")
+ )
+
+ filled_exposed_dropdown.setAdapter(adapter)
+ })
+ }
+}
+
+/*
+@Composable
+fun ChannelContent(channel: Channel?) {
+
+ val typography = MaterialTheme.typography
+ val context = ContextAmbient.current
+
+ Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
+ if (channel != null) {
+ Row(modifier = LayoutGravity.Center) {
+
+ Text(text = "Channel ", modifier = LayoutGravity.Center)
+
+ if (channel.editable) {
+ // FIXME - limit to max length
+ StyledTextField(
+ value = channel.name,
+ onValueChange = { channel.name = it },
+ textStyle = typography.h4.copy(
+ color = palette.onSecondary.copy(alpha = 0.8f)
+ ),
+ imeAction = ImeAction.Done,
+ onImeActionPerformed = {
+ ChannelLog.errormsg("FIXME, implement channel edit button")
+ }
+ )
+ } else {
+ Text(
+ text = channel.name,
+ style = typography.h4
+ )
+ }
+ }
+
+ // simulated qr code
+ // val image = imageResource(id = R.drawable.qrcode)
+ val image = AndroidImage(channel.getChannelQR())
+
+ ScaledImage(
+ image = image,
+ modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
+ )
+
+ Text(
+ text = "Mode: ${channel.modemConfig.toHumanString()}",
+ modifier = LayoutGravity.Center + LayoutPadding(bottom = 16.dp)
+ )
+
+ Row(modifier = LayoutGravity.Center) {
+
+ 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 = {
+
+ }) {
+ VectorImage(
+ id = R.drawable.ic_twotone_share_24,
+ tint = palette.onBackground
+ )
+ }
+ }
+ }
+ }
+}
+
+*/
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Map.kt b/app/src/main/java/com/geeksville/mesh/ui/Map.kt
deleted file mode 100644
index 1ebc95b8b..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Map.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-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.geometry.LatLngBounds
-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.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 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 nodesWithPosition = NodeDB.nodes.values.filter { it.validPosition != null }
- val locations = nodesWithPosition.map { node ->
- val p = node.position!!
- mapLog.debug("Showing on map: $node")
-
- val f = Feature.fromGeometry(
- Point.fromLngLat(
- p.longitude,
- p.latitude
- )
- )
- node.user?.let {
- f.addStringProperty("name", it.longName)
- }
- f
- }
- 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(
- iconImage(markerImageId),
- iconAnchor(Property.ICON_ANCHOR_BOTTOM),
- iconAllowOverlap(true)
- )
-
- val labelLayer = SymbolLayer(labelLayerId, nodeSourceId).withProperties(
- textField(Expression.get("name")),
- textSize(12f),
- textColor(Color.RED),
- textVariableAnchor(arrayOf(TEXT_ANCHOR_TOP)),
- textJustify(TEXT_JUSTIFY_AUTO),
- textAllowOverlap(true)
- )
-
- 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
-
- if (nodesWithPosition.isNotEmpty()) {
- val update = if (nodesWithPosition.size >= 2) {
- // Multiple nodes, make them all fit on the map view
- val bounds = LatLngBounds.Builder()
-
- // Add all positions
- bounds.includes(nodesWithPosition.map { it.position!! }
- .map { LatLng(it.latitude, it.longitude) })
-
- CameraUpdateFactory.newLatLngBounds(bounds.build(), 150)
- } else {
- // Only one node, just zoom in on it
- val it = nodesWithPosition[0].position!!
-
- val cameraPos = CameraPosition.Builder().target(
- LatLng(it.latitude, it.longitude)
- ).zoom(9.0).build()
- CameraUpdateFactory.newCameraPosition(cameraPos)
- }
- map.animateCamera(update, 1000)
- }
- }
- }
-}
-
-
-@Preview
-@Composable
-fun previewMap() {
- // another bug? It seems modaldrawerlayout not yet supported in preview
- MaterialTheme(colors = palette) {
- MapContent()
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
new file mode 100644
index 000000000..5063a6045
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
@@ -0,0 +1,183 @@
+package com.geeksville.mesh.ui
+
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import com.geeksville.android.Logging
+import com.geeksville.mesh.NodeInfo
+import com.geeksville.mesh.R
+import com.geeksville.mesh.model.UIViewModel
+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.geometry.LatLngBounds
+import com.mapbox.mapboxsdk.maps.MapView
+import com.mapbox.mapboxsdk.maps.MapboxMap
+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.SymbolLayer
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
+
+
+class MapFragment : ScreenFragment("Map"), Logging {
+
+ private val model: UIViewModel by activityViewModels()
+
+ private val nodeSourceId = "node-positions"
+ private val nodeLayerId = "node-layer"
+ private val labelLayerId = "label-layer"
+ private val markerImageId = "my-marker-image"
+
+ private val nodePositions = GeoJsonSource(nodeSourceId)
+
+ private val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId).withProperties(
+ iconImage(markerImageId),
+ iconAnchor(Property.ICON_ANCHOR_BOTTOM),
+ iconAllowOverlap(true)
+ )
+
+ private val labelLayer = SymbolLayer(labelLayerId, nodeSourceId).withProperties(
+ textField(Expression.get("name")),
+ textSize(12f),
+ textColor(Color.RED),
+ textVariableAnchor(arrayOf(TEXT_ANCHOR_TOP)),
+ textJustify(TEXT_JUSTIFY_AUTO),
+ textAllowOverlap(true)
+ )
+
+
+ private fun onNodesChanged(map: MapboxMap, nodes: Collection) {
+ val nodesWithPosition = nodes.filter { it.validPosition != null }
+
+ /**
+ * Using the latest nodedb, generate geojson features
+ */
+ fun getCurrentNodes(): FeatureCollection {
+ // Find all nodes with valid locations
+
+ val locations = nodesWithPosition.map { node ->
+ val p = node.position!!
+ debug("Showing on map: $node")
+
+ val f = Feature.fromGeometry(
+ Point.fromLngLat(
+ p.longitude,
+ p.latitude
+ )
+ )
+ node.user?.let {
+ f.addStringProperty("name", it.longName)
+ }
+ f
+ }
+
+ return FeatureCollection.fromFeatures(locations)
+ }
+
+ fun zoomToNodes(map: MapboxMap) {
+ if (nodesWithPosition.isNotEmpty()) {
+ val update = if (nodesWithPosition.size >= 2) {
+ // Multiple nodes, make them all fit on the map view
+ val bounds = LatLngBounds.Builder()
+
+ // Add all positions
+ bounds.includes(nodes.map { it.position!! }
+ .map { LatLng(it.latitude, it.longitude) })
+
+ CameraUpdateFactory.newLatLngBounds(bounds.build(), 150)
+ } else {
+ // Only one node, just zoom in on it
+ val it = nodesWithPosition[0].position!!
+
+ val cameraPos = CameraPosition.Builder().target(
+ LatLng(it.latitude, it.longitude)
+ ).zoom(9.0).build()
+ CameraUpdateFactory.newCameraPosition(cameraPos)
+ }
+ map.animateCamera(update, 1000)
+ }
+ }
+
+ nodePositions.setGeoJson(getCurrentNodes()) // Update node positions
+ zoomToNodes(map)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? = inflater.inflate(R.layout.map_view, container, false)
+
+ lateinit var mapView: MapView
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ mapView = view.findViewById(R.id.mapView)
+ mapView.onCreate(savedInstanceState)
+
+ mapView.getMapAsync { map ->
+
+ // val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24)
+ val markerIcon = requireActivity().getDrawable(R.drawable.ic_twotone_person_pin_24)!!
+
+ map.setStyle(Style.OUTDOORS) { style ->
+ style.addSource(nodePositions)
+ style.addImage(markerImageId, markerIcon)
+ style.addLayer(nodeLayer)
+ style.addLayer(labelLayer)
+ }
+
+ model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { nodes ->
+ onNodesChanged(map, nodes.values)
+ })
+
+ //map.uiSettings.isScrollGesturesEnabled = true
+ //map.uiSettings.isZoomGesturesEnabled = true
+ }
+ }
+
+ override fun onPause() {
+ mapView.onPause()
+ super.onPause()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mapView.onStart()
+ }
+
+ override fun onStop() {
+ mapView.onStop()
+ super.onStop()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mapView.onResume()
+ }
+
+ override fun onDestroy() {
+ mapView.onDestroy()
+ super.onDestroy()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ mapView.onSaveInstanceState(outState)
+ super.onSaveInstanceState(outState)
+ }
+}
+
+
+
+
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt
index 2641ef5c7..c835b2aa1 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt
@@ -1,19 +1,13 @@
package com.geeksville.mesh.ui
-import androidx.compose.Composable
-import androidx.compose.state
-import androidx.ui.foundation.Text
-import androidx.ui.material.*
-import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
-import com.geeksville.mesh.R
-import com.geeksville.mesh.model.UIState
object UILog : Logging
-
+/*
val palette = lightColorPalette() // darkColorPalette()
+
@Composable
fun MeshApp() {
val (drawerState, onDrawerStateChange) = state { DrawerState.Closed }
@@ -73,3 +67,4 @@ private fun AppContent(openDrawer: () -> Unit) {
}
//}
}
+*/
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Messages.kt b/app/src/main/java/com/geeksville/mesh/ui/Messages.kt
deleted file mode 100644
index 75e5d2981..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Messages.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.compose.state
-import androidx.ui.core.Modifier
-import androidx.ui.foundation.Text
-import androidx.ui.foundation.VerticalScroller
-import androidx.ui.graphics.Color
-import androidx.ui.input.ImeAction
-import androidx.ui.layout.Column
-import androidx.ui.layout.LayoutPadding
-import androidx.ui.layout.LayoutSize
-import androidx.ui.layout.Row
-import androidx.ui.material.Emphasis
-import androidx.ui.material.MaterialTheme
-import androidx.ui.material.ProvideEmphasis
-import androidx.ui.text.TextStyle
-import androidx.ui.tooling.preview.Preview
-import androidx.ui.unit.dp
-import com.geeksville.mesh.model.MessagesState
-import com.geeksville.mesh.model.MessagesState.messages
-import com.geeksville.mesh.model.NodeDB
-import com.geeksville.mesh.model.TextMessage
-import java.text.SimpleDateFormat
-
-
-private val dateFormat = SimpleDateFormat("h:mm a")
-
-val TimestampEmphasis = object : Emphasis {
- override fun emphasize(color: Color) = color.copy(alpha = 0.25f)
-}
-
-
-/**
- * A pretty version the text, with user icon to the left, name and time of arrival (copy slack look and feel)
- */
-@Composable
-fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
- Row(modifier = modifier) {
- UserIcon(NodeDB.nodes[msg.from])
-
- Column(modifier = LayoutPadding(start = 12.dp)) {
- Row {
- val nodes = NodeDB.nodes
-
- // If we can't find the sender, just use the ID
- val node = nodes.get(msg.from)
- val user = node?.user
- val senderName = user?.longName ?: msg.from
- Text(text = senderName)
- ProvideEmphasis(emphasis = TimestampEmphasis) {
- Text(
- text = dateFormat.format(msg.date),
- modifier = LayoutPadding(start = 8.dp),
- style = MaterialTheme.typography.caption
- )
- }
- }
- if (msg.errorMessage != null)
- Text(text = msg.errorMessage, style = TextStyle(color = palette.error))
- else
- Text(text = msg.text)
- }
- }
-}
-
-
-@Composable
-fun MessagesContent() {
- analyticsScreen(name = "messages")
-
- Column(modifier = LayoutSize.Fill) {
-
- val sidePad = 8.dp
- val topPad = 4.dp
-
- VerticalScroller(
- modifier = LayoutWeight(1f)
- ) {
- Column {
- messages.forEach { msg ->
- MessageCard(
- msg, modifier = LayoutPadding(
- start = sidePad,
- end = sidePad,
- top = topPad,
- bottom = topPad
- )
- )
- }
- }
- }
-
- // Spacer(LayoutFlexible(1f))
-
- val message = state { "" }
- StyledTextField(
- value = message.value,
- onValueChange = { message.value = it },
- textStyle = TextStyle(
- color = palette.onSecondary.copy(alpha = 0.8f)
- ),
- imeAction = ImeAction.Send,
- onImeActionPerformed = {
- MessagesState.info("did IME action")
-
- val str = message.value
- MessagesState.sendMessage(str)
- message.value = "" // blow away the string the user just entered
- },
- hintText = "Type your message here..."
- )
- }
-}
-
-
-@Preview
-@Composable
-fun previewMessagesView() {
- // another bug? It seems modaldrawerlayout not yet supported in preview
- MaterialTheme(colors = palette) {
- MessagesContent()
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
new file mode 100644
index 000000000..600943e40
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
@@ -0,0 +1,280 @@
+package com.geeksville.mesh.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.geeksville.android.Logging
+import com.geeksville.mesh.R
+import com.geeksville.mesh.model.TextMessage
+import com.geeksville.mesh.model.UIViewModel
+import kotlinx.android.synthetic.main.adapter_message_layout.view.*
+import kotlinx.android.synthetic.main.messages_fragment.*
+
+// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
+fun EditText.on(actionId: Int, func: () -> Unit) {
+ setOnEditorActionListener { _, receivedActionId, _ ->
+
+ if (actionId == receivedActionId) {
+ func()
+ }
+
+ true
+ }
+}
+
+class MessagesFragment : ScreenFragment("Messages"), Logging {
+
+ private val model: UIViewModel by activityViewModels()
+
+ // Provide a direct reference to each of the views within a data item
+ // Used to cache the views within the item layout for fast access
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val username = itemView.username
+ val messageText = itemView.messageText
+ }
+
+ private val messagesAdapter = object : RecyclerView.Adapter() {
+
+ /**
+ * Called when RecyclerView needs a new [ViewHolder] of the given type to represent
+ * an item.
+ *
+ *
+ * This new ViewHolder should be constructed with a new View that can represent the items
+ * of the given type. You can either create a new View manually or inflate it from an XML
+ * layout file.
+ *
+ *
+ * The new ViewHolder will be used to display items of the adapter using
+ * [.onBindViewHolder]. Since it will be re-used to display
+ * different items in the data set, it is a good idea to cache references to sub views of
+ * the View to avoid unnecessary [View.findViewById] calls.
+ *
+ * @param parent The ViewGroup into which the new View will be added after it is bound to
+ * an adapter position.
+ * @param viewType The view type of the new View.
+ *
+ * @return A new ViewHolder that holds a View of the given view type.
+ * @see .getItemViewType
+ * @see .onBindViewHolder
+ */
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val inflater = LayoutInflater.from(requireContext())
+
+ // Inflate the custom layout
+
+ // Inflate the custom layout
+ val contactView: View = inflater.inflate(R.layout.adapter_message_layout, parent, false)
+
+ // Return a new holder instance
+ return ViewHolder(contactView)
+ }
+
+ /**
+ * Returns the total number of items in the data set held by the adapter.
+ *
+ * @return The total number of items in this adapter.
+ */
+ override fun getItemCount(): Int = messages.size
+
+ /**
+ * Called by RecyclerView to display the data at the specified position. This method should
+ * update the contents of the [ViewHolder.itemView] to reflect the item at the given
+ * position.
+ *
+ *
+ * Note that unlike [android.widget.ListView], RecyclerView will not call this method
+ * again if the position of the item changes in the data set unless the item itself is
+ * invalidated or the new position cannot be determined. For this reason, you should only
+ * use the `position` parameter while acquiring the related data item inside
+ * this method and should not keep a copy of it. If you need the position of an item later
+ * on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
+ * have the updated adapter position.
+ *
+ * Override [.onBindViewHolder] instead if Adapter can
+ * handle efficient partial bind.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the
+ * item at the given position in the data set.
+ * @param position The position of the item within the adapter's data set.
+ */
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val msg = messages[position]
+
+ val nodes = model.nodeDB.nodes.value!!
+
+ // If we can't find the sender, just use the ID
+ val node = nodes.get(msg.from)
+ val user = node?.user
+ holder.username.text = user?.shortName ?: msg.from
+
+ if (msg.errorMessage != null) {
+ // FIXME, set the style to show a red error message
+ holder.messageText.text = msg.errorMessage
+ } else {
+ holder.messageText.text = msg.text
+ }
+ }
+
+ private var messages = arrayOf()
+
+ /// Called when our node DB changes
+ fun onMessagesChanged(nodesIn: Collection) {
+ messages = nodesIn.toTypedArray()
+ notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages
+
+ // scroll to the last line
+ messageListView.scrollToPosition(this.itemCount - 1)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.messages_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ messageInputText.on(EditorInfo.IME_ACTION_DONE) {
+ debug("did IME action")
+
+ val str = messageInputText.text.toString()
+ model.messagesState.sendMessage(str)
+ messageInputText.setText("") // blow away the string the user just entered
+
+ // requireActivity().hideKeyboard()
+ }
+
+ messageListView.adapter = messagesAdapter
+ val layoutManager = LinearLayoutManager(requireContext())
+ layoutManager.stackFromEnd = true // We want the last rows to always be shown
+ messageListView.layoutManager = layoutManager
+
+ model.messagesState.messages.observe(viewLifecycleOwner, Observer { it ->
+ messagesAdapter.onMessagesChanged(it)
+ })
+ }
+}
+
+/*
+import androidx.compose.Composable
+import androidx.compose.state
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.VerticalScroller
+import androidx.ui.graphics.Color
+import androidx.ui.input.ImeAction
+import androidx.ui.layout.Column
+import androidx.ui.layout.LayoutPadding
+import androidx.ui.layout.LayoutSize
+import androidx.ui.layout.Row
+import androidx.ui.material.Emphasis
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.ProvideEmphasis
+import androidx.ui.text.TextStyle
+import androidx.ui.tooling.preview.Preview
+import androidx.ui.unit.dp
+import com.geeksville.mesh.model.MessagesState
+import com.geeksville.mesh.model.MessagesState.messages
+import com.geeksville.mesh.model.NodeDB
+import com.geeksville.mesh.model.TextMessage
+import java.text.SimpleDateFormat
+
+
+private val dateFormat = SimpleDateFormat("h:mm a")
+
+val TimestampEmphasis = object : Emphasis {
+ override fun emphasize(color: Color) = color.copy(alpha = 0.25f)
+}
+
+
+/// A pretty version the text, with user icon to the left, name and time of arrival (copy slack look and feel)
+@Composable
+fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
+ Row(modifier = modifier) {
+ UserIcon(NodeDB.nodes[msg.from])
+
+ Column(modifier = LayoutPadding(start = 12.dp)) {
+ Row {
+ val nodes = NodeDB.nodes
+
+ // If we can't find the sender, just use the ID
+ val node = nodes.get(msg.from)
+ val user = node?.user
+ val senderName = user?.longName ?: msg.from
+ Text(text = senderName)
+ ProvideEmphasis(emphasis = TimestampEmphasis) {
+ Text(
+ text = dateFormat.format(msg.date),
+ modifier = LayoutPadding(start = 8.dp),
+ style = MaterialTheme.typography.caption
+ )
+ }
+ }
+ if (msg.errorMessage != null)
+ Text(text = msg.errorMessage, style = TextStyle(color = palette.error))
+ else
+ Text(text = msg.text)
+ }
+ }
+}
+
+
+@Composable
+fun MessagesContent() {
+ Column(modifier = LayoutSize.Fill) {
+
+ val sidePad = 8.dp
+ val topPad = 4.dp
+
+ VerticalScroller(
+ modifier = LayoutWeight(1f)
+ ) {
+ Column {
+ messages.forEach { msg ->
+ MessageCard(
+ msg, modifier = LayoutPadding(
+ start = sidePad,
+ end = sidePad,
+ top = topPad,
+ bottom = topPad
+ )
+ )
+ }
+ }
+ }
+
+ // Spacer(LayoutFlexible(1f))
+
+ val message = state { "" }
+ StyledTextField(
+ value = message.value,
+ onValueChange = { message.value = it },
+ textStyle = TextStyle(
+ color = palette.onSecondary.copy(alpha = 0.8f)
+ ),
+ imeAction = ImeAction.Send,
+ onImeActionPerformed = {
+ MessagesState.info("did IME action")
+
+ val str = message.value
+ MessagesState.sendMessage(str)
+ message.value = "" // blow away the string the user just entered
+ },
+ hintText = "Type your message here..."
+ )
+ }
+}
+
+
+*/
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt
deleted file mode 100644
index 132b5f732..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/NodeInfoCard.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.ui.foundation.Text
-import androidx.ui.layout.*
-import androidx.ui.material.MaterialTheme
-import androidx.ui.material.ProvideEmphasis
-import androidx.ui.tooling.preview.Preview
-import androidx.ui.unit.dp
-import com.geeksville.mesh.NodeInfo
-import com.geeksville.mesh.R
-import com.geeksville.mesh.model.NodeDB
-import androidx.ui.core.Modifier as Modifier1
-
-
-/*
-@Composable
-fun NodeIcon(modifier: Modifier1 = Modifier1.None, node: NodeInfo) {
- Column {
- Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
- VectorImage(id = if (node.user?.shortName != null) R.drawable.person else R.drawable.help)
- }
-
- // Show our shortname if possible
- /* node.user?.shortName?.let {
- Text(it)
- } */
-
- }
-}
- */
-
-@Composable
-fun CompassHeading(modifier: Modifier1 = Modifier1.None, node: NodeInfo) {
- Column {
- if (node.position != null) {
- Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
- VectorImage(id = R.drawable.navigation)
- }
- } else Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
- VectorImage(id = R.drawable.help)
- }
- Text("2.3 km") // always reserve space for the distance even if we aren't showing it
- }
-}
-
-@Composable
-fun NodeHeading(node: NodeInfo) {
- ProvideEmphasis(emphasis = MaterialTheme.emphasisLevels.high) {
- Text(
- node.user?.longName ?: "unknown",
- style = MaterialTheme.typography.subtitle1
- //modifier = LayoutWidth.Fill
- )
- }
-}
-
-/**
- * An info card for a node:
- *
- * on left, the icon for the user (or shortname if that is all we have) (this includes user's distance and heading arrow)
- *
- * Middle is users fullname
- *
- */
-@Composable
-fun NodeInfoCard(node: NodeInfo) {
- // Text("Node: ${it.user?.longName}")
- Row(modifier = LayoutPadding(16.dp)) {
- UILog.debug("showing NodeInfo $node")
-
- UserIcon(
- modifier = LayoutPadding(start = 0.dp, top = 0.dp, end = 0.dp, bottom = 0.dp),
- user = node
- )
-
- NodeHeading(node)
-
- // FIXME - show compass instead
- // CompassHeading(node = node)
- }
-}
-
-@Preview
-@Composable
-fun nodeInfoPreview() {
- Column {
- NodeInfoCard(NodeDB.testNodes[0])
- NodeInfoCard(NodeDB.testNodeNoPosition)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt
new file mode 100644
index 000000000..d707ce881
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt
@@ -0,0 +1,21 @@
+package com.geeksville.mesh.ui
+
+import androidx.fragment.app.Fragment
+import com.geeksville.android.GeeksvilleApplication
+
+/**
+ * A fragment that represents a current 'screen' in our app.
+ *
+ * Useful for tracking analytics
+ */
+open class ScreenFragment(private val screenName: String) : Fragment() {
+ override fun onResume() {
+ super.onResume()
+ GeeksvilleApplication.analytics.sendScreenView(screenName)
+ }
+
+ override fun onPause() {
+ GeeksvilleApplication.analytics.endScreenView()
+ super.onPause()
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Settings.kt b/app/src/main/java/com/geeksville/mesh/ui/Settings.kt
deleted file mode 100644
index f8c1da885..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Settings.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.compose.state
-import androidx.ui.core.ContextAmbient
-import androidx.ui.foundation.Text
-import androidx.ui.input.ImeAction
-import androidx.ui.layout.*
-import androidx.ui.material.MaterialTheme
-import androidx.ui.text.TextStyle
-import androidx.ui.tooling.preview.Preview
-import androidx.ui.unit.dp
-import com.geeksville.android.Logging
-import com.geeksville.mesh.model.MessagesState
-import com.geeksville.mesh.model.UIState
-import com.geeksville.mesh.service.RadioInterfaceService
-
-
-object SettingsLog : Logging
-
-@Composable
-fun SettingsContent() {
- //val typography = MaterialTheme.typography()
-
- val context = ContextAmbient.current
- Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
-
- Row {
- Text("Your name ", modifier = LayoutGravity.Center)
-
- val name = state { UIState.ownerName }
- StyledTextField(
- value = name.value,
- onValueChange = { name.value = it },
- textStyle = TextStyle(
- color = palette.onSecondary.copy(alpha = 0.8f)
- ),
- imeAction = ImeAction.Done,
- onImeActionPerformed = {
- MessagesState.info("did IME action")
- val n = name.value.trim()
- if (n.isNotEmpty())
- UIState.setOwner(context, n)
- },
- hintText = "Type your name here...",
- modifier = LayoutGravity.Center
- )
- }
-
- BTScanScreen()
-
- val bonded = RadioInterfaceService.getBondedDeviceAddress(context) != null
- if (!bonded) {
-
- val typography = MaterialTheme.typography
-
- Text(
- text =
- """
- You haven't yet paired a Meshtastic compatible radio with this phone.
-
- This application is an early alpha release, if you find problems please post on our website chat.
-
- For more information see our web page - www.meshtastic.org.
- """.trimIndent(), style = typography.body2
- )
- }
- }
-}
-
-
-@Preview
-@Composable
-fun previewSettings() {
- // another bug? It seems modaldrawerlayout not yet supported in preview
- MaterialTheme(colors = palette) {
- SettingsContent()
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
new file mode 100644
index 000000000..90b4ade79
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -0,0 +1,296 @@
+package com.geeksville.mesh.ui
+
+import android.app.Application
+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.Bundle
+import android.os.ParcelUuid
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.RadioButton
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import com.geeksville.android.Logging
+import com.geeksville.android.hideKeyboard
+import com.geeksville.mesh.R
+import com.geeksville.mesh.model.UIViewModel
+import com.geeksville.mesh.service.RadioInterfaceService
+import com.geeksville.util.exceptionReporter
+import kotlinx.android.synthetic.main.settings_fragment.*
+
+
+class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
+
+ private val context = getApplication().applicationContext
+
+ init {
+ debug("BTScanModel created")
+ }
+
+ data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
+ // val isSelected get() = macAddress == selectedMacAddr
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ debug("BTScanModel cleared")
+ }
+
+ /// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
+ val bluetoothAdapter =
+ (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
+
+ var selectedMacAddr: String? = null
+ val errorText = object : MutableLiveData(null) {}
+
+
+ private var scanner: BluetoothLeScanner? = null
+
+ private val scanCallback = object : ScanCallback() {
+ override fun onScanFailed(errorCode: Int) {
+ val msg = "Unexpected bluetooth scan failure: $errorCode"
+ // error code2 seeems to be indicate hung bluetooth stack
+ errorText.value = msg
+ }
+
+ // For each device that appears in our scan, ask for its GATT, when the gatt arrives,
+ // check if it is an eligable device and store it in our list of candidates
+ // if that device later disconnects remove it as a candidate
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
+
+ val addr = result.device.address
+ // prevent logspam because weill get get lots of redundant scan results
+ val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
+ val oldDevs = devices.value!!
+ val oldEntry = oldDevs[addr]
+ if (oldEntry == null || oldEntry.bonded != isBonded) {
+ val entry = BTScanEntry(
+ result.device.name,
+ addr,
+ isBonded
+ )
+ debug("onScanResult ${entry}")
+
+ // If nothing was selected, by default select the first thing we see
+ if (selectedMacAddr == null && entry.bonded)
+ changeSelection(context, addr)
+
+ devices.value = oldDevs + Pair(addr, entry) // trigger gui updates
+ }
+ }
+ }
+
+ fun stopScan() {
+ if (scanner != null) {
+ debug("stopping scan")
+ try {
+ scanner?.stopScan(scanCallback)
+ } catch (ex: Throwable) {
+ warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
+ }
+ scanner = null
+ }
+ }
+
+ fun startScan() {
+ debug("BTScan component active")
+ selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
+
+ if (bluetoothAdapter == null) {
+ warn("No bluetooth adapter. Running under emulation?")
+
+ val testnodes = listOf(
+ BTScanEntry("Meshtastic_ab12", "xx", false),
+ BTScanEntry("Meshtastic_32ac", "xb", true)
+ )
+
+ devices.value = (testnodes.map { it.macAddress to it }).toMap()
+
+ // If nothing was selected, by default select the first thing we see
+ if (selectedMacAddr == null)
+ changeSelection(context, testnodes.first().macAddress)
+ } else {
+ /// The following call might return null if the user doesn't have bluetooth access permissions
+ val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
+
+ if (s == null) {
+ errorText.value =
+ "This application requires bluetooth access. Please grant access in android settings."
+ } else {
+ debug("starting scan")
+
+ // filter and only accept devices that have a sw update service
+ val filter =
+ ScanFilter.Builder()
+ .setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
+ .build()
+
+ val settings =
+ ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build()
+ s.startScan(listOf(filter), settings, scanCallback)
+ scanner = s
+ }
+ }
+ }
+
+ val devices = object : MutableLiveData>(mapOf()) {
+
+
+ /**
+ * Called when the number of active observers change from 1 to 0.
+ *
+ *
+ * This does not mean that there are no observers left, there may still be observers but their
+ * lifecycle states aren't [Lifecycle.State.STARTED] or [Lifecycle.State.RESUMED]
+ * (like an Activity in the back stack).
+ *
+ *
+ * You can check if there are observers via [.hasObservers].
+ */
+ override fun onInactive() {
+ super.onInactive()
+ stopScan()
+ }
+ }
+
+ /// Called by the GUI when a new device has been selected by the user
+ /// Returns true if we were able to change to that item
+ fun onSelected(it: BTScanEntry): Boolean {
+ // If the device is paired, let user select it, otherwise start the pairing flow
+ if (it.bonded) {
+ changeSelection(context, it.macAddress)
+ return true
+ } else {
+ 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
+ )
+ debug("Received bond state changed $state")
+ context.unregisterReceiver(this)
+ if (state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_BONDING) {
+ debug("Bonding completed, connecting service")
+ 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)
+ ?.createBond()
+
+ return false
+ }
+ }
+
+ /// Change to a new macaddr selection, updating GUI and radio
+ fun changeSelection(context: Context, newAddr: String) {
+ info("Changing BT device to $newAddr")
+ selectedMacAddr = newAddr
+ RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
+ }
+}
+
+
+class SettingsFragment : ScreenFragment("Settings"), Logging {
+
+ private val scanModel: BTScanModel by activityViewModels()
+ private val model: UIViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.settings_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ model.ownerName.observe(viewLifecycleOwner, Observer { name ->
+ usernameEditText.setText(name)
+ })
+
+ usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
+ debug("did IME action")
+ val n = usernameEditText.text.toString().trim()
+ if (n.isNotEmpty())
+ model.setOwner(requireContext(), n)
+
+ requireActivity().hideKeyboard()
+ }
+
+ analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
+ // FIXME, preserve this in settings
+ analyticsOkayCheckbox.isChecked = true // so users will complain and I'll fix the bug
+ }
+
+ scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
+ if (errMsg != null) {
+ scanStatusText.text = errMsg
+ }
+ })
+
+ scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
+ // Remove the old radio buttons and repopulate
+ deviceRadioGroup.removeAllViews()
+
+ devices.values.forEach { device ->
+ val b = RadioButton(requireActivity())
+ b.text = device.name
+ b.id = View.generateViewId()
+ b.isEnabled =
+ true // Now we always want to enable, if the user clicks we'll try to bond device.bonded
+ b.isSelected = device.macAddress == scanModel.selectedMacAddr
+ deviceRadioGroup.addView(b)
+
+ b.setOnClickListener {
+ b.isChecked = scanModel.onSelected(device)
+ }
+ }
+
+ val hasBonded = RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
+
+ // get rid of the warning text once at least one device is paired
+ warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
+ })
+ }
+
+ override fun onPause() {
+ super.onPause()
+ scanModel.stopScan()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ scanModel.startScan()
+ }
+}
+
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Status.kt b/app/src/main/java/com/geeksville/mesh/ui/Status.kt
deleted file mode 100644
index dbe4bb64c..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Status.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Model
-import com.geeksville.mesh.R
-
-
-data class ScreenInfo(val icon: Int, val label: String)
-
-// defines the screens we have in the app
-object Screen {
- val settings = ScreenInfo(R.drawable.ic_twotone_settings_applications_24, "Settings")
- 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")
-}
-
-
-@Model
-object AppStatus {
- var currentScreen: ScreenInfo = Screen.messages
-}
-
-
-/**
- * Temporary solution pending navigation support.
- */
-fun navigateTo(destination: ScreenInfo) {
- AppStatus.currentScreen = destination
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt b/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt
deleted file mode 100644
index b3c3b4fb6..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/StyledTextField.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.compose.state
-import androidx.ui.core.Modifier
-import androidx.ui.foundation.TextField
-import androidx.ui.foundation.shape.corner.RoundedCornerShape
-import androidx.ui.graphics.Color
-import androidx.ui.input.ImeAction
-import androidx.ui.input.KeyboardType
-import androidx.ui.input.VisualTransformation
-import androidx.ui.layout.LayoutPadding
-import androidx.ui.material.Emphasis
-import androidx.ui.material.MaterialTheme
-import androidx.ui.material.ProvideEmphasis
-import androidx.ui.material.Surface
-import androidx.ui.text.TextStyle
-import androidx.ui.unit.dp
-
-
-val HintEmphasis = object : Emphasis {
- override fun emphasize(color: Color) = color.copy(alpha = 0.05f)
-}
-
-
-/// A text field that visually conveys that it is editable - FIXME, once Compose has material
-/// design text fields use that instead.
-@Composable
-fun StyledTextField(
- value: String,
- modifier: Modifier = Modifier.None,
- onValueChange: (String) -> Unit = {},
- textStyle: TextStyle = TextStyle.Default,
- keyboardType: KeyboardType = KeyboardType.Text,
- imeAction: ImeAction = ImeAction.Unspecified,
- onFocus: () -> Unit = {},
- onBlur: () -> Unit = {},
- focusIdentifier: String? = null,
- onImeActionPerformed: (ImeAction) -> Unit = {},
- visualTransformation: VisualTransformation? = null,
- hintText: String = ""
-) {
- val backgroundColor = palette.secondary.copy(alpha = 0.12f)
- Surface(
- modifier = LayoutPadding(8.dp),
- color = backgroundColor,
- shape = RoundedCornerShape(4.dp)
- ) {
- val showingHint = state { value.isEmpty() }
- val level = if (showingHint.value) HintEmphasis else MaterialTheme.emphasisLevels.medium
-
- ProvideEmphasis(level) {
- TextField(
- value.ifEmpty { if (showingHint.value) hintText else "" },
- modifier + LayoutPadding(4.dp),
- onValueChange,
- textStyle,
- keyboardType,
- imeAction,
- {
- showingHint.value = false // Stop showing the hint now
- onFocus()
- },
- {
- // if the string is empty again, return to the hint text
- showingHint.value = value.isEmpty()
- onBlur()
- },
- focusIdentifier,
- onImeActionPerformed,
- visualTransformation
- )
- }
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/UserIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/UserIcon.kt
deleted file mode 100644
index ceee5d748..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/UserIcon.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.ui.core.Modifier
-import androidx.ui.foundation.Text
-import androidx.ui.layout.Column
-import androidx.ui.layout.LayoutGravity
-import androidx.ui.layout.LayoutWidth
-import androidx.ui.material.MaterialTheme
-import androidx.ui.tooling.preview.Preview
-import androidx.ui.unit.dp
-import com.geeksville.mesh.NodeInfo
-import com.geeksville.mesh.R
-import com.geeksville.mesh.model.NodeDB
-
-/**
- * Show the user icon for a particular user with distance from the operator and a small pointer
- * indicating their direction
- *
- * This component is fixed width to simplify layouts.
- */
-@Composable
-fun UserIcon(user: NodeInfo? = null, modifier: Modifier = Modifier.None) {
- Column(modifier = modifier + LayoutWidth(60.dp)) {
- VectorImage(
- id = R.drawable.ic_twotone_person_24,
- tint = palette.onSecondary,
- modifier = LayoutGravity.Center
- )
- val ourNodeInfo = NodeDB.ourNodeInfo
- val distance = ourNodeInfo?.distanceStr(user)
- if (distance != null)
- Text(distance, modifier = LayoutGravity.Center)
- }
-}
-
-@Preview
-@Composable
-fun previewUserIcon() {
- // another bug? It seems modaldrawerlayout not yet supported in preview
- MaterialTheme(colors = palette) {
- UserIcon(NodeDB.testNodes[1])
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Users.kt b/app/src/main/java/com/geeksville/mesh/ui/Users.kt
deleted file mode 100644
index 08d49e992..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Users.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.compose.Composable
-import androidx.ui.core.ContextAmbient
-import androidx.ui.foundation.Text
-import androidx.ui.layout.Column
-import androidx.ui.layout.LayoutPadding
-import androidx.ui.layout.Row
-import androidx.ui.material.Button
-import androidx.ui.unit.dp
-import com.geeksville.mesh.R
-import com.geeksville.mesh.model.NodeDB
-import com.geeksville.mesh.model.UIState
-import com.geeksville.mesh.service.MeshService
-import com.geeksville.mesh.service.RadioInterfaceService
-import com.geeksville.mesh.service.SoftwareUpdateService
-
-
-/// Given a human name, strip out the first letter of the first three words and return that as the initials for
-/// that user.
-fun getInitials(name: String): String {
- val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }.take(3).map { it.first() }
- .joinToString("")
-
- return words
-}
-
-@Composable
-fun UsersContent() {
- analyticsScreen(name = "users")
-
- Column {
- Row {
- fun connected() = UIState.isConnected.value != MeshService.ConnectionState.DISCONNECTED
- VectorImage(
- id = if (connected()) R.drawable.cloud_on else R.drawable.cloud_off,
- tint = palette.onBackground,
- modifier = LayoutPadding(start = 8.dp)
- )
-
- Column {
-
- Text(
- when (UIState.isConnected.value) {
- MeshService.ConnectionState.CONNECTED -> "Connected"
- MeshService.ConnectionState.DISCONNECTED -> "Disconnected"
- MeshService.ConnectionState.DEVICE_SLEEP -> "Power Saving"
- },
- modifier = LayoutPadding(start = 8.dp)
- )
-
- if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet
- /// Create a software update button
- val context = ContextAmbient.current
- RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress ->
- Button(
- onClick = {
- SoftwareUpdateService.enqueueWork(
- context,
- SoftwareUpdateService.startUpdateIntent(macAddress)
- )
- }
- ) {
- Text(text = "Update firmware")
- }
- }
- }
- }
- }
-
- NodeDB.nodes.values.forEach {
- NodeInfoCard(it)
- }
-
-
- /* FIXME - doens't work yet - probably because I'm not using release keys
- // If account is null, then show the signin button, otherwise
- val context = ambient(ContextAmbient)
- val account = GoogleSignIn.getLastSignedInAccount(context)
- if (account != null)
- Text("We have an account")
- else {
- Text("No account yet")
- if (context is Activity) {
- Button("Google sign-in", onClick = {
- val signInIntent: Intent = UIState.googleSignInClient.signInIntent
- context.startActivityForResult(signInIntent, MainActivity.RC_SIGN_IN)
- })
- }
- } */
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
new file mode 100644
index 000000000..91c9e0e81
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
@@ -0,0 +1,182 @@
+package com.geeksville.mesh.ui
+
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.geeksville.android.Logging
+import com.geeksville.mesh.NodeInfo
+import com.geeksville.mesh.R
+import com.geeksville.mesh.model.UIViewModel
+import kotlinx.android.synthetic.main.adapter_node_layout.view.*
+import kotlinx.android.synthetic.main.nodelist_fragment.*
+
+
+class UsersFragment : ScreenFragment("Users"), Logging {
+
+ private val model: UIViewModel by activityViewModels()
+
+ // Provide a direct reference to each of the views within a data item
+ // Used to cache the views within the item layout for fast access
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val nodeNameView = itemView.nodeNameView
+ val distance_view = itemView.distance_view
+ }
+
+ private val nodesAdapter = object : RecyclerView.Adapter() {
+
+ /**
+ * Called when RecyclerView needs a new [ViewHolder] of the given type to represent
+ * an item.
+ *
+ *
+ * This new ViewHolder should be constructed with a new View that can represent the items
+ * of the given type. You can either create a new View manually or inflate it from an XML
+ * layout file.
+ *
+ *
+ * The new ViewHolder will be used to display items of the adapter using
+ * [.onBindViewHolder]. Since it will be re-used to display
+ * different items in the data set, it is a good idea to cache references to sub views of
+ * the View to avoid unnecessary [View.findViewById] calls.
+ *
+ * @param parent The ViewGroup into which the new View will be added after it is bound to
+ * an adapter position.
+ * @param viewType The view type of the new View.
+ *
+ * @return A new ViewHolder that holds a View of the given view type.
+ * @see .getItemViewType
+ * @see .onBindViewHolder
+ */
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val inflater = LayoutInflater.from(requireContext())
+
+ // Inflate the custom layout
+
+ // Inflate the custom layout
+ val contactView: View = inflater.inflate(R.layout.adapter_node_layout, parent, false)
+
+ // Return a new holder instance
+ return ViewHolder(contactView)
+ }
+
+ /**
+ * Returns the total number of items in the data set held by the adapter.
+ *
+ * @return The total number of items in this adapter.
+ */
+ override fun getItemCount(): Int = nodes.size
+
+ /**
+ * Called by RecyclerView to display the data at the specified position. This method should
+ * update the contents of the [ViewHolder.itemView] to reflect the item at the given
+ * position.
+ *
+ *
+ * Note that unlike [android.widget.ListView], RecyclerView will not call this method
+ * again if the position of the item changes in the data set unless the item itself is
+ * invalidated or the new position cannot be determined. For this reason, you should only
+ * use the `position` parameter while acquiring the related data item inside
+ * this method and should not keep a copy of it. If you need the position of an item later
+ * on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
+ * have the updated adapter position.
+ *
+ * Override [.onBindViewHolder] instead if Adapter can
+ * handle efficient partial bind.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the
+ * item at the given position in the data set.
+ * @param position The position of the item within the adapter's data set.
+ */
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val n = nodes[position]
+
+ holder.nodeNameView.text = n.user?.longName ?: n.user?.id ?: "Unknown node"
+
+ val ourNodeInfo = model.nodeDB.ourNodeInfo
+ val distance = ourNodeInfo?.distanceStr(n)
+ if (distance != null) {
+ holder.distance_view.text = distance
+ holder.distance_view.visibility = View.VISIBLE
+ } else {
+ holder.distance_view.visibility = View.INVISIBLE
+ }
+ }
+
+ private var nodes = arrayOf()
+
+ /// Called when our node DB changes
+ fun onNodesChanged(nodesIn: Collection) {
+ nodes = nodesIn.toTypedArray()
+ notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.nodelist_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ nodeListView.adapter = nodesAdapter
+ nodeListView.layoutManager = LinearLayoutManager(requireContext())
+
+ model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { it ->
+ nodesAdapter.onNodesChanged(it.values)
+ })
+ }
+}
+
+
+/*
+
+
+ if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet
+ /// Create a software update button
+ val context = ContextAmbient.current
+ RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress ->
+ Button(
+ onClick = {
+ SoftwareUpdateService.enqueueWork(
+ context,
+ SoftwareUpdateService.startUpdateIntent(macAddress)
+ )
+ }
+ ) {
+ Text(text = "Update firmware")
+ }
+ }
+ }
+ }
+ }
+
+
+
+ /* FIXME - doens't work yet - probably because I'm not using release keys
+ // If account is null, then show the signin button, otherwise
+ val context = ambient(ContextAmbient)
+ val account = GoogleSignIn.getLastSignedInAccount(context)
+ if (account != null)
+ Text("We have an account")
+ else {
+ Text("No account yet")
+ if (context is Activity) {
+ Button("Google sign-in", onClick = {
+ val signInIntent: Intent = UIState.googleSignInClient.signInIntent
+ context.startActivityForResult(signInIntent, MainActivity.RC_SIGN_IN)
+ })
+ }
+ } */
+ }
+}
+
+*/
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt b/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt
deleted file mode 100644
index 9aa4fb313..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/Vectors.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.geeksville.mesh.ui
-
-import androidx.annotation.DrawableRes
-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.layout.Container
-import androidx.ui.layout.LayoutSize
-import androidx.ui.material.IconButton
-import androidx.ui.res.vectorResource
-
-
-@Composable
-fun VectorImageButton(@DrawableRes id: Int, onClick: () -> Unit) {
- //Ripple(bounded = false) {
- IconButton(onClick = onClick) {
- Icon(vectorResource(id) /* , modifier = LayoutSize(40.dp, 40.dp) */)
- }
- //}
-}
-
-/* fun AppBarIcon(icon: Image, onClick: () -> Unit) {
- Container(width = ActionIconDiameter, height = ActionIconDiameter) {
- Ripple(bounded = false) {
- Clickable(onClick = onClick) {
- SimpleImage(icon)
- }
- }
- }
-} */
-
-@Composable
-fun VectorImage(
- modifier: Modifier = Modifier.None, @DrawableRes id: Int,
- tint: Color = Color.Transparent
-) {
- val vector = vectorResource(id)
- // WithDensity {
- Container(
- modifier = modifier + LayoutSize(
- vector.defaultWidth,
- vector.defaultHeight
- ) + drawVector(vector, tint)
- ) {
- }
- // }
-}
diff --git a/app/src/main/res/drawable/ic_twotone_cloud_upload_24.xml b/app/src/main/res/drawable/ic_twotone_cloud_upload_24.xml
new file mode 100644
index 000000000..8982a35a8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_twotone_cloud_upload_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/sl_lock_24dp.xml b/app/src/main/res/drawable/sl_lock_24dp.xml
new file mode 100644
index 000000000..49aeeda17
--- /dev/null
+++ b/app/src/main/res/drawable/sl_lock_24dp.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..faade1452
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_message_layout.xml b/app/src/main/res/layout/adapter_message_layout.xml
new file mode 100644
index 000000000..1010e0ddc
--- /dev/null
+++ b/app/src/main/res/layout/adapter_message_layout.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_node_layout.xml b/app/src/main/res/layout/adapter_node_layout.xml
new file mode 100644
index 000000000..60a894d15
--- /dev/null
+++ b/app/src/main/res/layout/adapter_node_layout.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/channel_fragment.xml b/app/src/main/res/layout/channel_fragment.xml
new file mode 100644
index 000000000..70a1ebcce
--- /dev/null
+++ b/app/src/main/res/layout/channel_fragment.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dropdown_menu_popup_item.xml b/app/src/main/res/layout/dropdown_menu_popup_item.xml
new file mode 100644
index 000000000..1909d58d1
--- /dev/null
+++ b/app/src/main/res/layout/dropdown_menu_popup_item.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
new file mode 100644
index 000000000..696be4e41
--- /dev/null
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/map_view.xml b/app/src/main/res/layout/map_view.xml
index a77c3ecf9..bfdb57e79 100644
--- a/app/src/main/res/layout/map_view.xml
+++ b/app/src/main/res/layout/map_view.xml
@@ -1,10 +1,17 @@
-
+ android:id="@+id/mapFrame">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/messages_fragment.xml b/app/src/main/res/layout/messages_fragment.xml
new file mode 100644
index 000000000..33a271e3f
--- /dev/null
+++ b/app/src/main/res/layout/messages_fragment.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/nodelist_fragment.xml b/app/src/main/res/layout/nodelist_fragment.xml
new file mode 100644
index 000000000..4e26df295
--- /dev/null
+++ b/app/src/main/res/layout/nodelist_fragment.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml
new file mode 100644
index 000000000..c68655d8c
--- /dev/null
+++ b/app/src/main/res/layout/settings_fragment.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 14f0f21d7..cd2b1a390 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,24 @@
- Meshtastic
+ Meshtastic
Settings
+ Channel Name
+ Channel options
+ Share button
+ QR code
+ Unset
+ Connection status
+ application icon
+ Unknown Username
+ User avatar
+ 2.13 km
+ hey I found the cache, it is over here next to the big tiger. I\'m kinda scared.
+ Some Username
+ Send Text
+ You haven\'t yet paired a Meshtastic compatible radio with this phone. Please pair a device and set your username.\n\nThis open-source application is in alpha-testing, if you find problems please post on our website chat.\n\nFor more information see our web page - www.meshtastic.org.
+ Username unset
+ Your Name
+ Anonymous usage statistics and crash reports.
+ Looking for Meshtastic devices...
+ Meshtastic_ac23
+ Meshtastic_1267
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 5e399fa2e..3431ac63d 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,14 +1,15 @@
-
-
@@ -17,4 +18,15 @@
+
+
+
diff --git a/build.gradle b/build.gradle
index c00cf0268..7c9083db8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,8 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.3.61'
- ext.compose_version = '0.1.0-dev08'
+ ext.kotlin_version = '1.3.71'
ext.coroutines_version = "1.3.5"
repositories {
@@ -11,7 +10,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.0-alpha04'
+ classpath 'com.android.tools.build:gradle:4.0.0-beta04'
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
@@ -23,7 +22,7 @@ buildscript {
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta03'
// protobuf plugin - docs here https://github.com/google/protobuf-gradle-plugin
- classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.11'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
}
}
diff --git a/geeksville-androidlib b/geeksville-androidlib
index 65f39f90c..ebc40c05f 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit 65f39f90ce365263620d5f9cbddca0c8abebcf9a
+Subproject commit ebc40c05fd8c30aabb3070468627e5fe6ae59cd5