Skip to content
Snippets Groups Projects
Commit acc93db9 authored by Adimote's avatar Adimote
Browse files

Update the UI for the device scanner

- Add a modal message for 0 devices showing
- Show the state of the connection next to every device

New 'scanning' screen, which shows when 0 devices are seen:
https://i.imgur.com/qgkox1A.jpg

Status indicators for each device:
https://i.imgur.com/dUI0iCq.jpg
(either 'NOT BONDED', 'BONDING...' or 'BONDED', The lattermost is bold and has green text)
parent ed9053bc
Branches
Tags
No related merge requests found
Pipeline #3414 passed
...@@ -20,11 +20,11 @@ ...@@ -20,11 +20,11 @@
* SOFTWARE. * SOFTWARE.
*/ */
package de.ccc.events.badge.card10.scanner; package de.ccc.events.badge.card10.scanner
import java.util.* import java.util.*
data class Device(val btMac: String, val name: String?, val paired: Boolean) { data class Device(val btMac: String, val name: String?, val state: Int) {
override fun equals(other: Any?): Boolean{ override fun equals(other: Any?): Boolean{
if (this === other) return true if (this === other) return true
if (other?.javaClass != javaClass) return false if (other?.javaClass != javaClass) return false
......
...@@ -22,10 +22,12 @@ ...@@ -22,10 +22,12 @@
package de.ccc.events.badge.card10.scanner package de.ccc.events.badge.card10.scanner
import android.bluetooth.BluetoothDevice
import android.graphics.Typeface import android.graphics.Typeface
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import de.ccc.events.badge.card10.R import de.ccc.events.badge.card10.R
...@@ -34,16 +36,22 @@ class DeviceViewHolder(inflater: LayoutInflater, parent: ViewGroup) : ...@@ -34,16 +36,22 @@ class DeviceViewHolder(inflater: LayoutInflater, parent: ViewGroup) :
private var nameView: TextView? = null private var nameView: TextView? = null
private var btMacView: TextView? = null private var btMacView: TextView? = null
private var statusView: TextView? = null
init { init {
nameView = itemView.findViewById(R.id.scanner_list_item_name) nameView = itemView.findViewById(R.id.scanner_list_item_name)
btMacView = itemView.findViewById(R.id.scanner_list_item_btmac) btMacView = itemView.findViewById(R.id.scanner_list_item_btmac)
statusView = itemView.findViewById(R.id.scanner_list_item_status)
} }
fun bind(device: Device, clickListener: (Device) -> Unit) { fun bind(device: Device, clickListener: (Device) -> Unit) {
nameView?.text = device.name nameView?.text = device.name
btMacView?.text = device.btMac btMacView?.text = device.btMac
btMacView?.typeface = if (device.paired) Typeface.DEFAULT_BOLD else Typeface.DEFAULT val typeface = if (device.state == BluetoothDevice.BOND_BONDED) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
btMacView?.typeface = typeface
statusView?.text = if (device.state == BluetoothDevice.BOND_BONDING) "BONDING..." else if (device.state == BluetoothDevice.BOND_BONDED) "BONDED" else "NOT BONDED"
statusView?.setTextColor( if (device.state == BluetoothDevice.BOND_BONDED) ContextCompat.getColor(itemView.context, R.color.colorStatusConnected) else ContextCompat.getColor(itemView.context, R.color.colorStatusDisconnected))
btMacView?.typeface = typeface
itemView.setOnClickListener { itemView.setOnClickListener {
clickListener(device) clickListener(device)
} }
......
...@@ -38,9 +38,11 @@ import androidx.fragment.app.Fragment ...@@ -38,9 +38,11 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.Fade
import de.ccc.events.badge.card10.CARD10_BLUETOOTH_MAC_PREFIX import de.ccc.events.badge.card10.CARD10_BLUETOOTH_MAC_PREFIX
import de.ccc.events.badge.card10.R
import kotlinx.android.synthetic.main.scanner_fragment.* import kotlinx.android.synthetic.main.scanner_fragment.*
import androidx.transition.TransitionManager
import de.ccc.events.badge.card10.R
class ScannerFragment : Fragment() { class ScannerFragment : Fragment() {
...@@ -52,45 +54,66 @@ class ScannerFragment : Fragment() { ...@@ -52,45 +54,66 @@ class ScannerFragment : Fragment() {
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device val device = result.device
if (device.address.startsWith(CARD10_BLUETOOTH_MAC_PREFIX, true)) if (device.address.startsWith(CARD10_BLUETOOTH_MAC_PREFIX, true))
putToListAdapter(device) upsertToListAdapter(device)
} }
} }
private fun putToListAdapter(device: BluetoothDevice) { private fun upsertToListAdapter(device: BluetoothDevice) {
listAdapter.put( if (listAdapter.itemCount == 0) {
viewModel.showModal.value = false
}
listAdapter.upsert(
Device( Device(
btMac = device.address, btMac = device.address,
name = device.name, name = device.name,
paired = device.bondState == BluetoothDevice.BOND_BONDED state = device.bondState
) )
) )
} }
// TODO: Remove devices if we lose their connection.
val receiver = object : BroadcastReceiver() { val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val bondState = intent.extras!!.getInt(BluetoothDevice.EXTRA_BOND_STATE)
val device = intent.extras!!.getParcelable(BluetoothDevice.EXTRA_DEVICE) as BluetoothDevice? val device = intent.extras!!.getParcelable(BluetoothDevice.EXTRA_DEVICE) as BluetoothDevice?
viewModel.status.value = ?: return
if (bondState == BluetoothDevice.BOND_NONE) "NONE" else if (bondState == BluetoothDevice.BOND_BONDING) "BONDING" else if (bondState == BluetoothDevice.BOND_BONDED) "BONDED" else null val status =
System.out.println("=== onReceive ${intent.action} ${viewModel.status.value} ${device}") if (device.bondState == BluetoothDevice.BOND_NONE) "NONE" else if (device.bondState == BluetoothDevice.BOND_BONDING) "BONDING" else if (device.bondState == BluetoothDevice.BOND_BONDED) "BONDED" else "NULL"
System.out.println("=== onReceive ${intent.action} ${status} ${device}")
upsertToListAdapter(device)
listAdapter.notifyDataSetChanged() listAdapter.notifyDataSetChanged()
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.status.observe(
viewModel.modalMessage.observe(
this, this,
Observer { text -> scanner_status.text = text }) Observer { text -> scanner_modal.text = text })
viewModel.showModal.observe(this, Observer { show ->
val transition = Fade()
transition.duration = 1000
transition.targets.add(scanner_modal_scrim)
TransitionManager.beginDelayedTransition(scanner_device_list, transition)
scanner_modal_scrim.visibility = if (show) View.VISIBLE else View.GONE
scanner_modal_scrim.isClickable = show
scanner_modal_scrim.isFocusable = show
})
activity?.registerReceiver(receiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) activity?.registerReceiver(receiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
for (device in bluetoothAdapter.bondedDevices.filter { val devices = bluetoothAdapter.bondedDevices.filter {
it.address.startsWith( it.address.startsWith(
CARD10_BLUETOOTH_MAC_PREFIX, CARD10_BLUETOOTH_MAC_PREFIX,
true true
) )
}) { }
putToListAdapter(device) for (device in devices) {
upsertToListAdapter(device)
}
// TODO: If we the ability to remove devices, show this modal if the list is empty.
if (devices.isEmpty()) {
viewModel.modalMessage.value = context?.resources?.getString(R.string.scan_no_devices)
viewModel.showModal.value = true
} }
bluetoothAdapter.bluetoothLeScanner.startScan(callback) bluetoothAdapter.bluetoothLeScanner.startScan(callback)
} }
......
...@@ -31,7 +31,6 @@ import kotlin.collections.HashSet ...@@ -31,7 +31,6 @@ import kotlin.collections.HashSet
class ScannerListAdapter(val clickListener: (Device) -> Unit) : RecyclerView.Adapter<DeviceViewHolder>() { class ScannerListAdapter(val clickListener: (Device) -> Unit) : RecyclerView.Adapter<DeviceViewHolder>() {
private val list = LinkedList<Device>() private val list = LinkedList<Device>()
private val foundDevices = mutableSetOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
...@@ -45,15 +44,12 @@ class ScannerListAdapter(val clickListener: (Device) -> Unit) : RecyclerView.Ada ...@@ -45,15 +44,12 @@ class ScannerListAdapter(val clickListener: (Device) -> Unit) : RecyclerView.Ada
override fun getItemCount(): Int = list.size override fun getItemCount(): Int = list.size
fun put(device: Device) { fun upsert(device: Device) {
if (foundDevices.contains(device.btMac)) { if (list.contains(device)) {
return list[list.indexOf(device)] = device
} else { } else {
foundDevices.add(device.btMac)
}
list.remove(device)
list.add(device) list.add(device)
}
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
...@@ -26,5 +26,6 @@ import androidx.lifecycle.MutableLiveData ...@@ -26,5 +26,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
class ScannerViewModel : ViewModel() { class ScannerViewModel : ViewModel() {
val status = MutableLiveData<String>() val modalMessage = MutableLiveData<String>()
val showModal = MutableLiveData<Boolean>()
} }
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:padding="16dp"> >
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_margin="16dp"
android:id="@+id/scanner_device_list" android:id="@+id/scanner_device_list"
android:layout_weight="1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0px"/> android:layout_height="match_parent"/>
<TextView android:id="@+id/scanner_status" <FrameLayout
android:id="@+id/scanner_modal_scrim"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="match_parent"
</LinearLayout> android:clickable="true"
android:focusable="true"
android:visibility="gone"
android:background="@color/colorScannerTint">
<TextView android:id="@+id/scanner_modal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="10dp"
android:background="@color/colorScannerBG"
/>
</FrameLayout>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_gravity="center_vertical"
android:layout_gravity="center_horizontal" android:foreground="?android:attr/selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:foreground="?android:attr/selectableItemBackground"> android:orientation="horizontal">
<LinearLayout
android:layout_width="0px"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/scanner_list_item_name" android:id="@+id/scanner_list_item_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="left" android:layout_gravity="start"
android:paddingLeft="16dp" android:paddingStart="16dp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/scanner_list_item_btmac" android:id="@+id/scanner_list_item_btmac"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="left" android:layout_gravity="start"
android:paddingLeft="16dp"/> android:paddingStart="16dp" />
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/scanner_list_item_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
</LinearLayout>
...@@ -3,4 +3,8 @@ ...@@ -3,4 +3,8 @@
<color name="colorPrimary">#0076ba</color> <color name="colorPrimary">#0076ba</color>
<color name="colorPrimaryDark">#005383</color> <color name="colorPrimaryDark">#005383</color>
<color name="colorAccent">#99ba00</color> <color name="colorAccent">#99ba00</color>
<color name="colorStatusDisconnected">#333333</color>
<color name="colorStatusConnected">#179317</color>
<color name="colorScannerTint">#55000000</color>
<color name="colorScannerBG">#ffffff</color>
</resources> </resources>
...@@ -50,4 +50,5 @@ ...@@ -50,4 +50,5 @@
<string name="camp">Camp</string> <string name="camp">Camp</string>
<string name="no_contact">No Contact</string> <string name="no_contact">No Contact</string>
<string name="off">Off</string> <string name="off">Off</string>
<string name="scan_no_devices">Scanning for devices…</string>
</resources> </resources>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment