diff --git a/app/build.gradle b/app/build.gradle index e83225dfd..dfe11bfc4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt index 96ce62a21..1b23b7d4c 100644 --- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt +++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 555a887e3..52a5152c1 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 { diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index f0a81085a..77d26af77 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index ef983d06d..c7d03f68f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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") } 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 fb3b113b9..ecc3cfe76 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -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() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt index 6257f6b5c..75822cdb2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt @@ -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(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(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) } + }) + } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt index 2093dfcc8..dbec8b65e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -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 }) } 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 c7c401db6..889792f5b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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() diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index c3bad63f0..0b6529679 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -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)