Merge branch 'master' into master

This commit is contained in:
Kevin Hester 2021-04-15 12:04:37 +08:00 committed by GitHub
commit 82ebfd0094
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1099 additions and 607 deletions

View file

@ -1,5 +1,6 @@
package com.geeksville.mesh.ui
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
@ -18,7 +19,6 @@ import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.ChannelFragmentBinding
import com.geeksville.mesh.model.Channel
@ -50,6 +50,7 @@ fun ImageView.setOpaque() {
class ChannelFragment : ScreenFragment("Channel"), Logging {
private var _binding: ChannelFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
@ -81,6 +82,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
val channels = model.channels.value
val channel = channels?.primaryChannel
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
binding.editableCheckbox.isChecked = false // start locked
if (channel != null) {
binding.qrView.visibility = View.VISIBLE
@ -89,7 +96,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
// For now, we only let the user edit/save channels while the radio is awake - because the service
// doesn't cache radioconfig writes.
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
binding.editableCheckbox.isEnabled = connected
binding.qrView.setImageBitmap(channels.getChannelQR())
@ -138,8 +144,38 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
requireActivity().startActivity(shareIntent)
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()
}
}
}
/// 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()
}
}
@ -150,13 +186,34 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
requireActivity().hideKeyboard()
}
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) { _, _ ->
debug("Switching back to default channel")
installSettings(Channel.default.settings)
}
.show()
}
// 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 ->
binding.channelNameEdit.setText(ch.name)
// 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
@ -170,49 +227,33 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
// 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()
newSettings.name = binding.channelNameEdit.text.toString().trim()
val newName = binding.channelNameEdit.text.toString().trim()
// Generate a new AES256 key unleess the user is trying to go back to stock
if (!newSettings.name.equals(
Channel.defaultChannel.name,
ignoreCase = true
)
) {
// 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
// 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.defaultChannel.settings.toBuilder()
newSettings = Channel.default.settings.toBuilder()
}
val selectedChannelOptionString =
binding.filledExposedDropdown.editableText.toString()
val modemConfig = getModemConfig(selectedChannelOptionString)
// No matter what apply the speed selection from the user
newSettings.modemConfig = modemConfig
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
newSettings.modemConfig = modemConfig
val newChannel = newSettings.build()
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()
}
installSettings(newSettings.build())
}
}
.show()

View file

@ -16,6 +16,7 @@ import com.geeksville.mesh.model.UIViewModel
class DebugFragment : Fragment() {
private var _binding: DebugFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
@ -44,11 +45,11 @@ class DebugFragment : Fragment() {
model.deleteAllPacket()
}
binding.closeButton.setOnClickListener{
parentFragmentManager.popBackStack();
binding.closeButton.setOnClickListener {
parentFragmentManager.popBackStack()
}
model.allPackets.observe(viewLifecycleOwner, Observer {
packets -> packets?.let { adapter.setPackets(it) }
model.allPackets.observe(viewLifecycleOwner, Observer { packets ->
packets?.let { adapter.setPackets(it) }
})
}
}

View file

@ -131,8 +131,10 @@ fun positionToMeter(a: MeshProtos.Position, b: MeshProtos.Position): Double {
a.latitudeI * 1e-7,
a.longitudeI * 1e-7,
b.latitudeI * 1e-7,
b.longitudeI * 1e-7)
b.longitudeI * 1e-7
)
}
/**
* Convert degrees/mins/secs to a single double
*
@ -186,7 +188,7 @@ fun bearing(
val y = sin(deltaLonRad) * cos(lat2Rad)
val x =
cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad)
* Math.cos(deltaLonRad))
* Math.cos(deltaLonRad))
return radToBearing(Math.atan2(y, x))
}

View file

@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
import com.geeksville.mesh.databinding.MessagesFragmentBinding
@ -25,7 +24,6 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip
import java.text.DateFormat
import java.text.ParseException
import java.util.*
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
@ -54,9 +52,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
private val timeFormat: DateFormat =
DateFormat.getTimeInstance(DateFormat.SHORT)
private fun getShortDateTime(time : Date): String {
private fun getShortDateTime(time: Date): String {
// return time if within 24 hours, otherwise date/time
val one_day = 60*60*24*100L
val one_day = 60 * 60 * 24 * 100L
if (System.currentTimeMillis() - time.time > one_day) {
return dateTimeFormat.format(time)
} else return timeFormat.format(time)
@ -152,11 +150,25 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
if (isMe) {
marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0
context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg)) }
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMyMsg
)
)
}
} else {
marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0
context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg)) }
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMsg
)
)
}
}
// Hide the username chip for my messages
if (isMe) {
@ -240,20 +252,26 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
fun updateTextEnabled() {
binding.textInputLayout.isEnabled =
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null && model.radioConfig.value != null
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED
// Just being connected is enough to allow sending texts I think
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
}
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
updateTextEnabled() })
updateTextEnabled()
})
model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
/* model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
updateTextEnabled() })
updateTextEnabled()
})
model.radioConfig.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
updateTextEnabled() })
updateTextEnabled()
}) */
}
}

View file

@ -1,49 +1,50 @@
package com.geeksville.mesh.ui
import android.content.Context
import java.text.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.Packet
import java.util.*
class PacketListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var packets = emptyList<Packet>()
private val timeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val packetTypeView: TextView = itemView.findViewById(R.id.type)
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
val packetRawMessage : TextView = itemView.findViewById(R.id.rawMessage)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
return PacketViewHolder(itemView)
}
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
val current = packets[position]
holder.packetTypeView.text = current.message_type
holder.packetRawMessage.text = current.raw_message
val date = Date(current.received_date)
holder.packetDateReceivedView.text = timeFormat.format(date)
}
internal fun setPackets(packets: List<Packet>) {
this.packets = packets
notifyDataSetChanged()
}
override fun getItemCount() = packets.size
package com.geeksville.mesh.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.Packet
import java.text.DateFormat
import java.util.*
class PacketListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var packets = emptyList<Packet>()
private val timeFormat: DateFormat =
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val packetTypeView: TextView = itemView.findViewById(R.id.type)
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
val packetRawMessage: TextView = itemView.findViewById(R.id.rawMessage)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
return PacketViewHolder(itemView)
}
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
val current = packets[position]
holder.packetTypeView.text = current.message_type
holder.packetRawMessage.text = current.raw_message
val date = Date(current.received_date)
holder.packetDateReceivedView.text = timeFormat.format(date)
}
internal fun setPackets(packets: List<Packet>) {
this.packets = packets
notifyDataSetChanged()
}
override fun getItemCount() = packets.size
}

View file

@ -38,10 +38,7 @@ import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.android.usbManager
import com.geeksville.mesh.databinding.SettingsFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.BluetoothInterface
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.mesh.service.SerialInterface
import com.geeksville.mesh.service.*
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressSuccess
@ -59,7 +56,7 @@ import kotlinx.coroutines.Job
import java.util.regex.Pattern
object SLogging : Logging {}
object SLogging : Logging
/// Change to a new macaddr selection, updating GUI and radio
fun changeDeviceSelection(context: MainActivity, newAddr: String?) {
@ -186,7 +183,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
// if that device later disconnects remove it as a candidate
override fun onScanResult(callbackType: Int, result: ScanResult) {
if ((result.device.name?.startsWith("Mesh") ?: false)) {
if ((result.device.name?.startsWith("Mesh") == true)) {
val addr = result.device.address
val fullAddr = "x$addr" // full address with the bluetooh prefix
// prevent logspam because weill get get lots of redundant scan results
@ -245,14 +242,12 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
debug("BTScan component active")
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
return if (bluetoothAdapter == null || RadioInterfaceService.isMockInterfaceAvailable(
context
)
) {
return if (bluetoothAdapter == null || MockInterface.addressValid(context, "")) {
warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
DeviceListEntry("Simulated interface", "m", true),
DeviceListEntry("Included simulator", "m", true),
DeviceListEntry("Complete simulator", "t10.0.2.2", true),
DeviceListEntry(context.getString(R.string.none), "n", true)
/* Don't populate fake bluetooth devices, because we don't want testlab inside of google
to try and use them.
@ -494,7 +489,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
/// Set the correct update button configuration based on current progress
private fun refreshUpdateButton() {
private fun refreshUpdateButton(enable: Boolean) {
debug("Reiniting the udpate button")
val info = model.myNodeInfo.value
val service = model.meshService
@ -505,7 +500,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val progress = service.updateStatus
binding.updateFirmwareButton.isEnabled =
binding.updateFirmwareButton.isEnabled = enable &&
(progress < 0) // if currently doing an upgrade disable button
if (progress >= 0) {
@ -542,7 +537,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val connected = model.isConnected.value
val isConnected = connected == MeshService.ConnectionState.CONNECTED
binding.nodeSettings.visibility = if(isConnected) View.VISIBLE else View.GONE
binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE
if (connected == MeshService.ConnectionState.DISCONNECTED)
model.ownerName.value = ""
@ -552,25 +547,19 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val spinner = binding.regionSpinner
val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
spinner.onItemSelectedListener = null
if(region != null) {
debug("current region is $region")
var regionIndex = regions.indexOf(region.name)
if(regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
regionIndex = unsetIndex
// We don't want to be notified of our own changes, so turn off listener while making them
spinner.setSelection(regionIndex, false)
spinner.onItemSelectedListener = regionSpinnerListener
spinner.isEnabled = true
}
else {
warn("region is unset!")
spinner.setSelection(unsetIndex, false)
spinner.isEnabled = false // leave disabled, because we can't get our region
}
debug("current region is $region")
var regionIndex = regions.indexOf(region.name)
if (regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
regionIndex = unsetIndex
// We don't want to be notified of our own changes, so turn off listener while making them
spinner.setSelection(regionIndex, false)
spinner.onItemSelectedListener = regionSpinnerListener
spinner.isEnabled = true
// If actively connected possibly let the user update firmware
refreshUpdateButton()
refreshUpdateButton(region != RadioConfigProtos.RegionCode.Unset)
// Update the status string (highest priority messages first)
val info = model.myNodeInfo.value
@ -590,14 +579,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener{
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View,
position: Int,
id: Long
) {
val item = parent.getItemAtPosition(position) as String
val item = parent.getItemAtPosition(position) as String?
val asProto = item!!.let { RadioConfigProtos.RegionCode.valueOf(it) }
exceptionToSnackbar(requireView()) {
model.region = asProto
@ -622,7 +611,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// init our region spinner
val spinner = binding.regionSpinner
val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
val regionAdapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = regionAdapter
@ -632,7 +622,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// Only let user edit their name or set software update while connected to a radio
model.isConnected.observe(viewLifecycleOwner, Observer { connectionState ->
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
updateNodeInfo()
})
@ -702,7 +692,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
scanModel.onSelected(requireActivity() as MainActivity, device)
if (!b.isSelected)
binding.scanStatusText.setText(getString(R.string.please_pair))
binding.scanStatusText.text = getString(R.string.please_pair)
}
}
@ -765,7 +755,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// get rid of the warning text once at least one device is paired.
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
binding.warningNotPaired.visibility =
if (hasBonded && !RadioInterfaceService.isMockInterfaceAvailable(requireContext())) View.GONE else View.VISIBLE
if (hasBonded && !MockInterface.addressValid(requireContext(), ""))
View.GONE
else
View.VISIBLE
}
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
@ -935,7 +928,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
refreshUpdateButton()
refreshUpdateButton(true)
}
}

View file

@ -113,7 +113,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
val name = n.user?.longName ?: n.user?.id ?: "Unknown node"
holder.nodeNameView.text = name
val pos = n.validPosition;
val pos = n.validPosition
if (pos != null) {
val coords =
String.format("%.5f %.5f", pos.latitude, pos.longitude).replace(",", ".")
@ -141,7 +141,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
}
renderBattery(n.batteryPctLevel, holder)
holder.lastTime.text = formatAgo(n.lastSeen);
holder.lastTime.text = formatAgo(n.lastHeard)
if ((n.num == ourNodeInfo?.num) || (n.snr > 100f)) {
holder.signalView.visibility = View.INVISIBLE