diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 9f6f85edc..a1c990d69 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -385,9 +385,8 @@ class MainActivity : AppCompatActivity(), Logging,
if (connected == MeshService.ConnectionState.CONNECTED) {
// everytime the radio reconnects, we slam in our current owner data, the radio is smart enough to only broadcast if needed
- model.setOwner(this)
-
-
+ model.setOwner()
+
model.meshService?.let { service ->
debug("Getting latest radioconfig from service")
model.radioConfig.value =
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 d3c0a6890..0209b2795 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
@@ -11,9 +11,7 @@ import java.net.MalformedURLException
data class Channel(
- var name: String,
- var modemConfig: MeshProtos.ChannelSettings.ModemConfig,
- var settings: MeshProtos.ChannelSettings? = MeshProtos.ChannelSettings.getDefaultInstance()
+ val settings: MeshProtos.ChannelSettings = MeshProtos.ChannelSettings.getDefaultInstance()
) {
companion object {
// Placeholder when emulating
@@ -37,10 +35,11 @@ data class Channel(
}
}
- constructor(c: MeshProtos.ChannelSettings) : this(c.name, c.modemConfig, c)
-
constructor(url: Uri) : this(urlToSettings(url))
+ val name: String get() = settings.name
+ val modemConfig: MeshProtos.ChannelSettings.ModemConfig get() = settings.modemConfig
+
/// Can this channel be changed right now?
var editable = false
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 1b5e7b435..7650f575f 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -1,12 +1,13 @@
package com.geeksville.mesh.model
+import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.os.RemoteException
import androidx.core.content.edit
+import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.android.Logging
import com.geeksville.mesh.IMeshService
@@ -22,7 +23,7 @@ fun getInitials(name: String): String {
return words
}
-class UIViewModel : ViewModel(), Logging {
+class UIViewModel(app: Application) : AndroidViewModel(app), Logging {
init {
debug("ViewModel created")
}
@@ -46,6 +47,8 @@ class UIViewModel : ViewModel(), Logging {
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
+ private val context = app.applicationContext
+
var meshService: IMeshService? = null
val nodeDB = NodeDB(this)
@@ -67,7 +70,7 @@ class UIViewModel : ViewModel(), Logging {
}
/// Set the radio config (also updates our saved copy in preferences)
- fun setRadioConfig(context: Context, c: MeshProtos.RadioConfig) {
+ fun setRadioConfig(c: MeshProtos.RadioConfig) {
debug("Setting new radio config!")
meshService?.radioConfig = c.toByteArray()
radioConfig.value = c
@@ -77,6 +80,15 @@ class UIViewModel : ViewModel(), Logging {
}
}
+ /** Update just the channel settings portion of our config (both in the device and in saved preferences) */
+ fun setChannel(c: MeshProtos.ChannelSettings) {
+ // When running on the emulator, radio config might not really be available, in that case, just ignore attempts to change the config
+ radioConfig.value?.toBuilder()?.let { config ->
+ config.channelSettings = c
+ setRadioConfig(config.build())
+ }
+ }
+
/// Kinda ugly - created in the activity but used from Compose - figure out if there is a cleaner way GIXME
// lateinit var googleSignInClient: GoogleSignInClient
@@ -91,7 +103,7 @@ class UIViewModel : ViewModel(), Logging {
var requestedChannelUrl: Uri? = null
// clean up all this nasty owner state management FIXME
- fun setOwner(context: Context, s: String? = null) {
+ fun setOwner(s: String? = null) {
if (s != null) {
ownerName.value = s
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index 4d8136f2f..54af5116c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -1,22 +1,42 @@
package com.geeksville.mesh.ui
import android.content.Intent
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
+import android.widget.ImageView
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.android.hideKeyboard
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
-import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.channel_fragment.*
+// Make an image view dim
+fun ImageView.setDim() {
+ val matrix = ColorMatrix()
+ matrix.setSaturation(0f) //0 means grayscale
+ val cf = ColorMatrixColorFilter(matrix)
+ colorFilter = cf
+ imageAlpha = 64 // 128 = 0.5
+}
+
+/// Return image view to normal
+fun ImageView.setOpaque() {
+ colorFilter = null
+ imageAlpha = 255
+}
+
class ChannelFragment : ScreenFragment("Channel"), Logging {
private val model: UIViewModel by activityViewModels()
@@ -28,77 +48,116 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
return inflater.inflate(R.layout.channel_fragment, container, false)
}
+ /// Called when the lock/unlock icon has changed
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
+ if (isEditing) // Dim the (stale) QR code while editing...
+ qrView.setDim()
+ else
+ qrView.setOpaque()
+ }
+
+ /// Pull the latest data from the model (discarding any user edits)
+ private fun setGUIfromModel() {
+ val channel = UIViewModel.getChannel(model.radioConfig.value)
+
+ editableCheckbox.isChecked = false // start locked
+ if (channel != null) {
+ qrView.visibility = View.VISIBLE
+ channelNameEdit.visibility = View.VISIBLE
+ channelNameEdit.setText(channel.name)
+ editableCheckbox.isEnabled = true
+
+ qrView.setImageBitmap(channel.getChannelQR())
+ } else {
+ qrView.visibility = View.INVISIBLE
+ channelNameEdit.visibility = View.INVISIBLE
+ editableCheckbox.isEnabled = false
+ }
+
+ onEditingChanged() // we just locked the gui
+
+ val adapter = ArrayAdapter(
+ requireContext(),
+ R.layout.dropdown_menu_popup_item,
+ arrayOf("Item 1", "Item 2", "Item 3", "Item 4")
+ )
+
+ filled_exposed_dropdown.setAdapter(adapter)
+ }
+
+ private fun shareChannel() {
+ UIViewModel.getChannel(model.radioConfig.value)?.let { channel ->
+
+ 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,
+ getString(R.string.url_for_join)
+ )
+ type = "text/plain"
+ }
+
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ requireActivity().startActivity(shareIntent)
+ }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- onEditingChanged() // Set initial state
+ channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
+ requireActivity().hideKeyboard()
+ }
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(
+ // User just locked it, we should warn and then apply changes to radio
+ /* Snackbar.make(
editableCheckbox,
"Changing channels is not yet supported",
Snackbar.LENGTH_SHORT
- ).show()
+ ).show() */
+
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle("Change channel")
+ .setMessage("Are you sure you want to change the channel? All communication with other nodes will stop until you share the new channel settings.")
+ .setNeutralButton("Cancel") { _, _ ->
+ setGUIfromModel()
+ }
+ .setPositiveButton("Accept") { _, _ ->
+ // Generate a new channel with only the changes the user can change in the GUI
+ UIViewModel.getChannel(model.radioConfig.value)?.let { old ->
+ val newSettings = old.settings.toBuilder()
+ newSettings.name = channelNameEdit.text.toString().trim()
+ // FIXME, regenerate a new preshared key!
+ model.setChannel(newSettings.build())
+ // Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
+ }
+ }
+ .show()
}
+
+ onEditingChanged() // update GUI on what user is allowed to edit/share
+ }
+
+ // Share this particular channel if someone clicks share
+ shareButton.setOnClickListener {
+ shareChannel()
}
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)
+ setGUIfromModel()
})
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
index fe0aadc5e..9b8b9065c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -254,7 +254,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
debug("did IME action")
val n = usernameEditText.text.toString().trim()
if (n.isNotEmpty())
- model.setOwner(requireContext(), n)
+ model.setOwner(n)
requireActivity().hideKeyboard()
}
diff --git a/app/src/main/res/layout/channel_fragment.xml b/app/src/main/res/layout/channel_fragment.xml
index 70a1ebcce..a8b198ede 100644
--- a/app/src/main/res/layout/channel_fragment.xml
+++ b/app/src/main/res/layout/channel_fragment.xml
@@ -21,6 +21,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/channel_name"
+ android:imeOptions="actionDone"
+ android:maxLength="12"
+ android:singleLine="true"
android:text="@string/unset" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63791e3ba..23fe37ab6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,4 +25,5 @@
Error - this app requires bluetooth
Starting pairing
Pairing failed
+ A URL for joining a Meshtastic mesh