Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt

324 lines
13 KiB
Kotlin
Raw Normal View History

2020-02-17 20:00:11 -08:00
package com.geeksville.mesh.ui
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
2021-11-15 10:54:10 -03:00
import android.net.Uri
2020-04-07 16:04:58 -07:00
import android.os.Bundle
import android.os.RemoteException
2020-04-07 16:04:58 -07:00
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
2020-04-07 17:42:31 -07:00
import android.widget.ArrayAdapter
import android.widget.ImageView
2020-04-07 17:42:31 -07:00
import androidx.fragment.app.activityViewModels
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
2020-02-18 08:56:37 -08:00
import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
2021-02-27 14:31:52 +08:00
import com.geeksville.mesh.AppOnlyProtos
2021-02-27 13:43:55 +08:00
import com.geeksville.mesh.ChannelProtos
2020-02-17 20:00:11 -08:00
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.ChannelFragmentBinding
import com.geeksville.mesh.model.Channel
2020-06-14 00:11:08 -04:00
import com.geeksville.mesh.model.ChannelOption
2021-02-27 14:31:52 +08:00
import com.geeksville.mesh.model.ChannelSet
2020-04-07 17:42:31 -07:00
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.protobuf.ByteString
2021-11-15 10:54:10 -03:00
import com.google.zxing.integration.android.IntentIntegrator
import java.net.MalformedURLException
import java.security.SecureRandom
2020-02-17 20:00:11 -08:00
// 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
}
2020-04-07 16:04:58 -07:00
class ChannelFragment : ScreenFragment("Channel"), Logging {
private var _binding: ChannelFragmentBinding? = null
2021-03-24 13:48:26 +08:00
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
2020-04-07 17:42:31 -07:00
private val model: UIViewModel by activityViewModels()
2020-04-07 16:04:58 -07:00
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
2020-04-07 17:42:31 -07:00
): View? {
_binding = ChannelFragmentBinding.inflate(inflater, container, false)
return binding.root
2020-04-07 17:42:31 -07:00
}
2020-04-07 16:04:58 -07:00
/// Called when the lock/unlock icon has changed
private fun onEditingChanged() {
val isEditing = binding.editableCheckbox.isChecked
binding.channelOptions.isEnabled = isEditing
binding.shareButton.isEnabled = !isEditing
2021-11-15 10:54:10 -03:00
binding.scanButton.isEnabled = isEditing
binding.channelNameView.isEnabled = isEditing
if (isEditing) // Dim the (stale) QR code while editing...
binding.qrView.setDim()
else
binding.qrView.setOpaque()
}
/// Pull the latest data from the model (discarding any user edits)
private fun setGUIfromModel() {
2021-02-27 13:43:55 +08:00
val channels = model.channels.value
val channel = channels?.primaryChannel
2021-03-24 13:48:26 +08:00
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
// Only let buttons work if we are connected to the radio
binding.shareButton.isEnabled = connected
binding.resetButton.isEnabled = connected && Channel.default != channel
2021-03-24 13:48:26 +08:00
binding.editableCheckbox.isChecked = false // start locked
if (channel != null) {
binding.qrView.visibility = View.VISIBLE
binding.channelNameEdit.visibility = View.VISIBLE
binding.channelNameEdit.setText(channel.humanName)
// For now, we only let the user edit/save channels while the radio is awake - because the service
// doesn't cache radioconfig writes.
binding.editableCheckbox.isEnabled = connected
val bitmap = channels.qrCode
if (bitmap != null)
binding.qrView.setImageBitmap(bitmap)
2021-02-27 13:43:55 +08:00
val modemConfig = channel.modemConfig
val channelOption = ChannelOption.fromConfig(modemConfig)
binding.filledExposedDropdown.setText(
getString(
channelOption?.configRes ?: R.string.modem_config_unrecognized
), false
)
} else {
binding.qrView.visibility = View.INVISIBLE
binding.channelNameEdit.visibility = View.INVISIBLE
binding.editableCheckbox.isEnabled = false
}
onEditingChanged() // we just locked the gui
2020-06-14 00:11:08 -04:00
val modemConfigs = ChannelOption.values()
val modemConfigList = modemConfigs.map { getString(it.configRes) }
val adapter = ArrayAdapter(
requireContext(),
R.layout.dropdown_menu_popup_item,
2020-06-14 00:11:08 -04:00
modemConfigList
)
binding.filledExposedDropdown.setAdapter(adapter)
}
private fun shareChannel() {
2021-02-27 13:43:55 +08:00
model.channels.value?.let { channels ->
GeeksvilleApplication.analytics.track(
"share",
DataPair("content_type", "channel")
) // track how many times users share channels
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
2021-02-27 13:43:55 +08:00
putExtra(Intent.EXTRA_TEXT, channels.getChannelUrl().toString())
putExtra(
Intent.EXTRA_TITLE,
getString(R.string.url_for_join)
)
type = "text/plain"
}
try {
val shareIntent = Intent.createChooser(sendIntent, null)
requireActivity().startActivity(shareIntent)
} catch (ex: ActivityNotFoundException) {
Snackbar.make(
binding.shareButton,
R.string.no_app_found,
Snackbar.LENGTH_SHORT
).show()
}
}
}
2021-03-24 13:48:26 +08:00
/// Send new channel settings to the device
private fun installSettings(newChannel: ChannelProtos.ChannelSettings) {
val newSet =
ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
// Try to change the radio, if it fails, tell the user why and throw away their redits
try {
model.setChannels(newSet)
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
} catch (ex: RemoteException) {
errormsg("ignoring channel problem", ex)
setGUIfromModel() // Throw away user edits
// Tell the user to try again
Snackbar.make(
binding.editableCheckbox,
R.string.radio_sleeping,
Snackbar.LENGTH_SHORT
).show()
}
}
2020-04-07 16:04:58 -07:00
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
2020-04-07 17:42:31 -07:00
binding.channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
requireActivity().hideKeyboard()
}
2021-03-24 13:48:26 +08:00
binding.resetButton.setOnClickListener { _ ->
// User just locked it, we should warn and then apply changes to radio
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.reset_to_defaults)
.setMessage(R.string.are_you_shure_change_default)
.setNeutralButton(R.string.cancel) { _, _ ->
setGUIfromModel() // throw away any edits
}
.setPositiveButton(R.string.apply) { _, _ ->
2021-03-24 13:48:26 +08:00
debug("Switching back to default channel")
installSettings(Channel.default.settings)
2021-03-24 13:48:26 +08:00
}
.show()
}
2021-11-15 10:54:10 -03:00
binding.scanButton.setOnClickListener {
val zxingScan = IntentIntegrator.forSupportFragment(this)
zxingScan.setCameraId(0)
zxingScan.setPrompt("")
zxingScan.setBeepEnabled(false)
zxingScan.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
zxingScan.initiateScan()
}
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
binding.editableCheckbox.setOnClickListener { _ ->
/// We use this to determine if the user tried to install a custom name
var originalName = ""
val checked = binding.editableCheckbox.isChecked
if (checked) {
// User just unlocked for editing - remove the # goo around the channel name
model.channels.value?.primaryChannel?.let { ch ->
// Note: We are careful to show the emptystring here if the user was on a default channel, so the user knows they should it for any changes
originalName = ch.settings.name
binding.channelNameEdit.setText(originalName)
}
} else {
// User just locked it, we should warn and then apply changes to radio
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.change_channel)
.setMessage(R.string.are_you_sure_channel)
.setNeutralButton(R.string.cancel) { _, _ ->
setGUIfromModel()
}
.setPositiveButton(getString(R.string.accept)) { _, _ ->
// Generate a new channel with only the changes the user can change in the GUI
model.channels.value?.primaryChannel?.let { oldPrimary ->
var newSettings = oldPrimary.settings.toBuilder()
val newName = binding.channelNameEdit.text.toString().trim()
// Find the new modem config
val selectedChannelOptionString =
binding.filledExposedDropdown.editableText.toString()
var modemConfig = getModemConfig(selectedChannelOptionString)
if (modemConfig == ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED) // Huh? didn't find it - keep same
modemConfig = oldPrimary.settings.modemConfig
2021-03-24 13:48:26 +08:00
// Generate a new AES256 key if the user changes channel name or the name is non-default and the settings changed
if (newName != originalName || (newName.isNotEmpty() && modemConfig != oldPrimary.settings.modemConfig)) {
// Install a new customized channel
debug("ASSIGNING NEW AES256 KEY")
val random = SecureRandom()
val bytes = ByteArray(32)
random.nextBytes(bytes)
newSettings.name = newName
newSettings.psk = ByteString.copyFrom(bytes)
} else {
debug("Switching back to default channel")
newSettings = Channel.default.settings.toBuilder()
}
// No matter what apply the speed selection from the user
newSettings.modemConfig = modemConfig
2021-03-24 13:48:26 +08:00
installSettings(newSettings.build())
}
}
.show()
2020-04-07 17:42:31 -07:00
}
onEditingChanged() // update GUI on what user is allowed to edit/share
}
2020-04-07 17:42:31 -07:00
// Share this particular channel if someone clicks share
binding.shareButton.setOnClickListener {
shareChannel()
}
2021-03-05 14:14:17 +08:00
model.channels.observe(viewLifecycleOwner, {
setGUIfromModel()
})
// If connection state changes, we might need to enable/disable buttons
model.isConnected.observe(viewLifecycleOwner, {
setGUIfromModel()
2020-04-07 17:42:31 -07:00
})
2020-04-07 16:04:58 -07:00
}
2020-06-14 00:11:08 -04:00
2021-02-27 13:43:55 +08:00
private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig {
2020-06-14 00:11:08 -04:00
for (item in ChannelOption.values()) {
if (getString(item.configRes) == selectedChannelOptionString)
return item.modemConfig
}
2021-02-27 13:43:55 +08:00
return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED
2020-06-14 00:11:08 -04:00
}
2021-11-15 10:54:10 -03:00
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result != null) {
if (result.contents == null) {
Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
} else {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = ChannelSet(Uri.parse(result.contents)).getChannelUrl(false)
startActivity(intent)
} catch (ex: ActivityNotFoundException) {
Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
} catch (ex: MalformedURLException) {
Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
}
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
}