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