switching away from kotlin-android-extensions

This commit is contained in:
Kevin Hester
2020-12-07 20:33:29 +08:00
parent cc2b99fdfc
commit 2e30dbcdd0
10 changed files with 191 additions and 170 deletions

View File

@@ -1,6 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.github.triplet.play'
@@ -59,6 +59,8 @@ android {
buildFeatures {
// Enables Jetpack Compose for this module
// compose true // NOTE, if true main app crashes if you use regular view layout functions
viewBinding true
}
// Set both the Java and Kotlin compilers to target Java 8.
@@ -85,10 +87,6 @@ play {
serviceAccountCredentials = file("../../play-credentials.json")
}
androidExtensions {
experimental = true
}
// per protobuf-gradle-plugin docs, this is recommended for android
protobuf {
protoc {

View File

@@ -2,7 +2,7 @@ package com.geeksville.mesh
import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize

View File

@@ -38,6 +38,7 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.*
@@ -54,7 +55,6 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.google.protobuf.InvalidProtocolBufferException
import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -124,6 +124,8 @@ class MainActivity : AppCompatActivity(), Logging,
13 // seems to be hardwired in CompanionDeviceManager to add 65536
}
private lateinit var binding: ActivityMainBinding
// Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
@@ -366,6 +368,8 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val prefs = UIViewModel.getPreferences(this)
model.ownerName.value = prefs.getString("owner", "")!!
@@ -396,15 +400,15 @@ class MainActivity : AppCompatActivity(), Logging,
/* setContent {
MeshApp()
} */
setContentView(R.layout.activity_main)
setContentView(binding.root)
initToolbar()
pager.adapter = tabsAdapter
pager.isUserInputEnabled =
binding.pager.adapter = tabsAdapter
binding.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 ->
TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->
// tab.text = tabInfos[position].text // I think it looks better with icons only
tab.icon = getDrawable(tabInfos[position].icon)
}.attach()
@@ -893,7 +897,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
private fun showSettingsPage() {
pager.currentItem = 5
binding.pager.currentItem = 5
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@@ -4,7 +4,7 @@ import android.os.Parcelable
import com.geeksville.mesh.ui.bearing
import com.geeksville.mesh.ui.latLongToMeter
import com.geeksville.util.anonymize
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

View File

@@ -591,7 +591,7 @@ class MeshService : Service(), Logging {
if (myInfo.myNodeNum == packet.from)
debug("Ignoring retransmission of our packet ${bytes.size}")
else {
debug("Received data from $fromId ${bytes.size}")
debug("Received data from $fromId, portnum=${data.portnumValue} ${bytes.size} bytes")
dataPacket.status = MessageStatus.RECEIVED
rememberDataPacket(dataPacket)
@@ -616,10 +616,7 @@ class MeshService : Service(), Logging {
val u = MeshProtos.User.parseFrom(data.payload)
handleReceivedUser(packet.from, u)
}
else -> {
debug("Received other data packet")
}}
}
// We always tell other apps when new data packets arrive
serviceBroadcasts.broadcastReceivedData(dataPacket)
@@ -1466,7 +1463,7 @@ class MeshService : Service(), Logging {
// Keep a record of datapackets, so GUIs can show proper chat history
rememberDataPacket(p)
if(p.bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
if (p.bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
p.status = MessageStatus.ERROR
throw RemoteException("Message too long")
}

View File

@@ -18,6 +18,7 @@ import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.ChannelFragmentBinding
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.ChannelOption
import com.geeksville.mesh.model.UIViewModel
@@ -25,7 +26,6 @@ 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
import kotlinx.android.synthetic.main.channel_fragment.*
import java.security.SecureRandom
@@ -46,26 +46,31 @@ 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!!
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)
_binding = ChannelFragmentBinding.inflate(inflater, container, false)
return binding.root
}
/// Called when the lock/unlock icon has changed
private fun onEditingChanged() {
val isEditing = editableCheckbox.isChecked
val isEditing = binding.editableCheckbox.isChecked
channelOptions.isEnabled = isEditing
shareButton.isEnabled = !isEditing
channelNameView.isEnabled = isEditing
binding.channelOptions.isEnabled = isEditing
binding.shareButton.isEnabled = !isEditing
binding.channelNameView.isEnabled = isEditing
if (isEditing) // Dim the (stale) QR code while editing...
qrView.setDim()
binding.qrView.setDim()
else
qrView.setOpaque()
binding.qrView.setOpaque()
}
/// Pull the latest data from the model (discarding any user edits)
@@ -73,31 +78,31 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
val radioConfig = model.radioConfig.value
val channel = UIViewModel.getChannel(radioConfig)
editableCheckbox.isChecked = false // start locked
binding.editableCheckbox.isChecked = false // start locked
if (channel != null) {
qrView.visibility = View.VISIBLE
channelNameEdit.visibility = View.VISIBLE
channelNameEdit.setText(channel.humanName)
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.
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
editableCheckbox.isEnabled = connected
binding.editableCheckbox.isEnabled = connected
qrView.setImageBitmap(channel.getChannelQR())
binding.qrView.setImageBitmap(channel.getChannelQR())
val modemConfig = radioConfig?.channelSettings?.modemConfig
val channelOption = ChannelOption.fromConfig(modemConfig)
filled_exposed_dropdown.setText(
binding.filledExposedDropdown.setText(
getString(
channelOption?.configRes ?: R.string.modem_config_unrecognized
), false
)
} else {
qrView.visibility = View.INVISIBLE
channelNameEdit.visibility = View.INVISIBLE
editableCheckbox.isEnabled = false
binding.qrView.visibility = View.INVISIBLE
binding.channelNameEdit.visibility = View.INVISIBLE
binding.editableCheckbox.isEnabled = false
}
onEditingChanged() // we just locked the gui
@@ -109,7 +114,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
modemConfigList
)
filled_exposed_dropdown.setAdapter(adapter)
binding.filledExposedDropdown.setAdapter(adapter)
}
private fun shareChannel() {
@@ -138,17 +143,17 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
binding.channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
requireActivity().hideKeyboard()
}
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
editableCheckbox.setOnClickListener { _ ->
val checked = editableCheckbox.isChecked
binding.editableCheckbox.setOnClickListener { _ ->
val checked = binding.editableCheckbox.isChecked
if (checked) {
// User just unlocked for editing - remove the # goo around the channel name
UIViewModel.getChannel(model.radioConfig.value)?.let { channel ->
channelNameEdit.setText(channel.name)
binding.channelNameEdit.setText(channel.name)
}
} else {
// User just locked it, we should warn and then apply changes to radio
@@ -162,7 +167,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
// 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()
newSettings.name = binding.channelNameEdit.text.toString().trim()
// Generate a new AES256 key (for any channel not named Default)
if (!newSettings.name.equals(
@@ -183,7 +188,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
val selectedChannelOptionString =
filled_exposed_dropdown.editableText.toString()
binding.filledExposedDropdown.editableText.toString()
val modemConfig = getModemConfig(selectedChannelOptionString)
if (modemConfig != MeshProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
@@ -199,7 +204,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
// Tell the user to try again
Snackbar.make(
editableCheckbox,
binding.editableCheckbox,
R.string.radio_sleeping,
Snackbar.LENGTH_SHORT
).show()
@@ -213,7 +218,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
// Share this particular channel if someone clicks share
shareButton.setOnClickListener {
binding.shareButton.setOnClickListener {
shareChannel()
}

View File

@@ -1,50 +1,54 @@
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.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import kotlinx.android.synthetic.main.debug_fragment.*
class DebugFragment : Fragment() {
val model: UIViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.debug_fragment, container, false)
}
//Button to clear All log
//List all log
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = view.findViewById<RecyclerView>(R.id.packets_recyclerview)
val adapter = PacketListAdapter(requireContext())
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
clearButton.setOnClickListener {
model.deleteAllPacket()
}
closeButton.setOnClickListener{
parentFragmentManager.popBackStack();
}
model.allPackets.observe(viewLifecycleOwner, Observer {
packets -> packets?.let { adapter.setPackets(it) }
})
}
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.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.DebugFragmentBinding
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!!
val model: UIViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DebugFragmentBinding.inflate(inflater, container, false)
return binding.root
}
//Button to clear All log
//List all log
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = view.findViewById<RecyclerView>(R.id.packets_recyclerview)
val adapter = PacketListAdapter(requireContext())
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.clearButton.setOnClickListener {
model.deleteAllPacket()
}
binding.closeButton.setOnClickListener{
parentFragmentManager.popBackStack();
}
model.allPackets.observe(viewLifecycleOwner, Observer {
packets -> packets?.let { adapter.setPackets(it) }
})
}
}

View File

@@ -16,11 +16,11 @@ import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
import com.geeksville.mesh.databinding.MessagesFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.adapter_message_layout.view.*
import kotlinx.android.synthetic.main.messages_fragment.*
import java.text.DateFormat
import java.util.*
@@ -38,13 +38,17 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
class MessagesFragment : ScreenFragment("Messages"), Logging {
private var _binding: MessagesFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val model: UIViewModel by activityViewModels()
private val dateTimeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
// 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) {
class ViewHolder(itemView: AdapterMessageLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val username: Chip = itemView.username
val messageText: TextView = itemView.messageText
val messageTime: TextView = itemView.messageTime
@@ -82,10 +86,10 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// Inflate the custom layout
// Inflate the custom layout
val contactView: View = inflater.inflate(R.layout.adapter_message_layout, parent, false)
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
return ViewHolder(contactViewBinding)
}
/**
@@ -159,7 +163,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// scroll to the last line
if (itemCount != 0)
messageListView.scrollToPosition(itemCount - 1)
binding.messageListView.scrollToPosition(itemCount - 1)
}
}
@@ -167,27 +171,28 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.messages_fragment, container, false)
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
messageInputText.on(EditorInfo.IME_ACTION_DONE) {
binding.messageInputText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
val str = messageInputText.text.toString().trim()
val str = binding.messageInputText.text.toString().trim()
if (str.isNotEmpty())
model.messagesState.sendMessage(str)
messageInputText.setText("") // blow away the string the user just entered
binding.messageInputText.setText("") // blow away the string the user just entered
// requireActivity().hideKeyboard()
}
messageListView.adapter = messagesAdapter
binding.messageListView.adapter = messagesAdapter
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true // We want the last rows to always be shown
messageListView.layoutManager = layoutManager
binding.messageListView.layoutManager = layoutManager
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
debug("New messages received: ${it.size}")
@@ -197,13 +202,13 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
model.isConnected.observe(viewLifecycleOwner, Observer { connected ->
// If we don't know our node ID and we are offline don't let user try to send
textInputLayout.isEnabled =
binding.textInputLayout.isEnabled =
connected != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null
})
model.nodeDB.myId.observe(viewLifecycleOwner, Observer { myId ->
// If we don't know our node ID and we are offline don't let user try to send
textInputLayout.isEnabled =
binding.textInputLayout.isEnabled =
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null
})
}

View File

@@ -34,6 +34,7 @@ import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
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
@@ -46,7 +47,6 @@ import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hoho.android.usbserial.driver.UsbSerialDriver
import kotlinx.android.synthetic.main.settings_fragment.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -452,6 +452,10 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
@SuppressLint("NewApi")
class SettingsFragment : ScreenFragment("Settings"), Logging {
private var _binding: SettingsFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val scanModel: BTScanModel by activityViewModels()
private val model: UIViewModel by activityViewModels()
@@ -478,23 +482,23 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
mainScope.handledLaunch {
debug("User started firmware update")
updateFirmwareButton.isEnabled = false // Disable until things complete
updateProgressBar.visibility = View.VISIBLE
updateProgressBar.progress = 0 // start from scratch
binding.updateFirmwareButton.isEnabled = false // Disable until things complete
binding.updateProgressBar.visibility = View.VISIBLE
binding.updateProgressBar.progress = 0 // start from scratch
scanStatusText.text = "Updating firmware, wait up to eight minutes..."
binding.scanStatusText.text = "Updating firmware, wait up to eight minutes..."
try {
service.startFirmwareUpdate()
while (service.updateStatus >= 0) {
updateProgressBar.progress = service.updateStatus
binding.updateProgressBar.progress = service.updateStatus
delay(2000) // Only check occasionally
}
} finally {
val isSuccess = (service.updateStatus == -1)
scanStatusText.text =
binding.scanStatusText.text =
if (isSuccess) "Update successful" else "Update failed"
updateProgressBar.isEnabled = false
updateFirmwareButton.isEnabled = !isSuccess
binding.updateProgressBar.isEnabled = false
binding.updateFirmwareButton.isEnabled = !isSuccess
}
}
}
@@ -504,7 +508,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.settings_fragment, container, false)
_binding = SettingsFragmentBinding.inflate(inflater, container, false)
return binding.root
}
private fun initNodeInfo() {
@@ -513,36 +518,36 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// If actively connected possibly let the user update firmware
val info = model.myNodeInfo.value
if (connected == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate) {
updateFirmwareButton.visibility = View.VISIBLE
updateFirmwareButton.isEnabled = true
updateFirmwareButton.text =
binding.updateFirmwareButton.visibility = View.VISIBLE
binding.updateFirmwareButton.isEnabled = true
binding.updateFirmwareButton.text =
getString(R.string.update_to).format(getString(R.string.cur_firmware_version))
} else {
updateFirmwareButton.visibility = View.GONE
updateProgressBar.visibility = View.GONE
binding.updateFirmwareButton.visibility = View.GONE
binding.updateProgressBar.visibility = View.GONE
}
when (connected) {
MeshService.ConnectionState.CONNECTED -> {
val fwStr = info?.firmwareString ?: ""
scanStatusText.text = getString(R.string.connected_to).format(fwStr)
binding.scanStatusText.text = getString(R.string.connected_to).format(fwStr)
}
MeshService.ConnectionState.DISCONNECTED ->
scanStatusText.text = getString(R.string.not_connected)
binding.scanStatusText.text = getString(R.string.not_connected)
MeshService.ConnectionState.DEVICE_SLEEP ->
scanStatusText.text = getString(R.string.connected_sleeping)
binding.scanStatusText.text = getString(R.string.connected_sleeping)
}
}
/// Setup the ui widgets unrelated to BLE scanning
private fun initCommonUI() {
model.ownerName.observe(viewLifecycleOwner, Observer { name ->
usernameEditText.setText(name)
binding.usernameEditText.setText(name)
})
// Only let user edit their name or set software update while connected to a radio
model.isConnected.observe(viewLifecycleOwner, Observer { connected ->
usernameView.isEnabled = connected == MeshService.ConnectionState.CONNECTED
binding.usernameView.isEnabled = connected == MeshService.ConnectionState.CONNECTED
if (connected == MeshService.ConnectionState.DISCONNECTED)
model.ownerName.value = ""
initNodeInfo()
@@ -553,13 +558,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
initNodeInfo()
})
updateFirmwareButton.setOnClickListener {
binding.updateFirmwareButton.setOnClickListener {
doFirmwareUpdate()
}
usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
binding.usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
val n = usernameEditText.text.toString().trim()
val n = binding.usernameEditText.text.toString().trim()
if (n.isNotEmpty())
model.setOwner(n)
@@ -569,17 +574,17 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val app = (requireContext().applicationContext as GeeksvilleApplication)
// Set analytics checkbox
analyticsOkayCheckbox.isChecked = app.isAnalyticsAllowed
binding.analyticsOkayCheckbox.isChecked = app.isAnalyticsAllowed
analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
binding.analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
debug("User changed analytics to $isChecked")
app.isAnalyticsAllowed = isChecked
reportBugButton.isEnabled = app.isAnalyticsAllowed
binding.reportBugButton.isEnabled = app.isAnalyticsAllowed
}
// report bug button only enabled if analytics is allowed
reportBugButton.isEnabled = app.isAnalyticsAllowed
reportBugButton.setOnClickListener {
binding.reportBugButton.isEnabled = app.isAnalyticsAllowed
binding.reportBugButton.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.report_a_bug)
.setMessage(getString(R.string.report_bug_text))
@@ -600,34 +605,34 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
b.isEnabled = enabled
b.isChecked =
device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired
deviceRadioGroup.addView(b)
binding.deviceRadioGroup.addView(b)
// Once we have at least one device, don't show the "looking for" animation - it makes uers think
// something is busted
scanProgressBar.visibility = View.INVISIBLE
binding.scanProgressBar.visibility = View.INVISIBLE
b.setOnClickListener {
if (!device.bonded) // If user just clicked on us, try to bond
scanStatusText.setText(R.string.starting_pairing)
binding.scanStatusText.setText(R.string.starting_pairing)
b.isChecked =
scanModel.onSelected(requireActivity() as MainActivity, device)
if (!b.isSelected)
scanStatusText.setText(getString(R.string.please_pair))
binding.scanStatusText.setText(getString(R.string.please_pair))
}
}
/// Show the GUI for classic scanning
private fun showClassicWidgets(visible: Int) {
scanProgressBar.visibility = visible
deviceRadioGroup.visibility = visible
binding.scanProgressBar.visibility = visible
binding.deviceRadioGroup.visibility = visible
}
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
private fun initClassicScan() {
// Turn off the widgets for the new API (we turn on/off hte classic widgets when we start scanning
changeRadioButton.visibility = View.GONE
binding.changeRadioButton.visibility = View.GONE
showClassicWidgets(View.VISIBLE)
@@ -640,13 +645,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
if (errMsg != null) {
scanStatusText.text = errMsg
binding.scanStatusText.text = errMsg
}
})
scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
// Remove the old radio buttons and repopulate
deviceRadioGroup.removeAllViews()
binding.deviceRadioGroup.removeAllViews()
val adapter = scanModel.bluetoothAdapter
@@ -690,14 +695,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
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
binding.warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
})
}
/// Start running the modern scan, once it has one result we enable the
private fun startBackgroundScan() {
// Disable the change button until our scan has some results
changeRadioButton.isEnabled = false
binding.changeRadioButton.isEnabled = false
// To skip filtering based on name and supported feature flags (UUIDs),
// don't include calls to setNamePattern() and addServiceUuid(),
@@ -726,8 +731,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
override fun onDeviceFound(chooserLauncher: IntentSender) {
debug("Found one device - enabling button")
changeRadioButton.isEnabled = true
changeRadioButton.setOnClickListener {
binding.changeRadioButton.isEnabled = true
binding.changeRadioButton.setOnClickListener {
debug("User clicked BLE change button")
// Request code seems to be ignored anyways
@@ -748,18 +753,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun initModernScan() {
// Turn off the widgets for the classic API
scanProgressBar.visibility = View.GONE
deviceRadioGroup.visibility = View.GONE
changeRadioButton.visibility = View.VISIBLE
binding.scanProgressBar.visibility = View.GONE
binding.deviceRadioGroup.visibility = View.GONE
binding.changeRadioButton.visibility = View.VISIBLE
val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext())
if (curRadio != null) {
scanStatusText.text = getString(R.string.current_pair).format(curRadio)
changeRadioButton.text = getString(R.string.change_radio)
binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
binding.changeRadioButton.text = getString(R.string.change_radio)
} else {
scanStatusText.text = getString(R.string.not_paired_yet)
changeRadioButton.setText(R.string.select_radio)
binding.scanStatusText.text = getString(R.string.not_paired_yet)
binding.changeRadioButton.setText(R.string.select_radio)
}
startBackgroundScan()

View File

@@ -14,22 +14,26 @@ import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
import com.geeksville.mesh.databinding.NodelistFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import kotlinx.android.synthetic.main.adapter_node_layout.view.*
import kotlinx.android.synthetic.main.nodelist_fragment.*
import java.text.ParseException
import java.util.*
class UsersFragment : ScreenFragment("Users"), Logging {
private var _binding: NodelistFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
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) {
class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val nodeNameView = itemView.nodeNameView
val distance_view = itemView.distance_view
val distanceView = itemView.distanceView
val batteryPctView = itemView.batteryPercentageView
val lastTime = itemView.lastConnectionView
val powerIcon = itemView.batteryIcon
@@ -64,9 +68,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
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)
val contactView = AdapterNodeLayoutBinding.inflate(inflater, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
@@ -108,10 +110,10 @@ class UsersFragment : ScreenFragment("Users"), Logging {
val ourNodeInfo = model.nodeDB.ourNodeInfo
val distance = ourNodeInfo?.distanceStr(n)
if (distance != null) {
holder.distance_view.text = distance
holder.distance_view.visibility = View.VISIBLE
holder.distanceView.text = distance
holder.distanceView.visibility = View.VISIBLE
} else {
holder.distance_view.visibility = View.INVISIBLE
holder.distanceView.visibility = View.INVISIBLE
}
renderBattery(n.batteryPctLevel, holder)
@@ -176,14 +178,15 @@ class UsersFragment : ScreenFragment("Users"), Logging {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.nodelist_fragment, container, false)
_binding = NodelistFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
nodeListView.adapter = nodesAdapter
nodeListView.layoutManager = LinearLayoutManager(requireContext())
binding.nodeListView.adapter = nodesAdapter
binding.nodeListView.layoutManager = LinearLayoutManager(requireContext())
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { it ->
nodesAdapter.onNodesChanged(it.values)