|
|
@@ -0,0 +1,458 @@
|
|
|
+package com.example.watch.ui.activity
|
|
|
+
|
|
|
+import android.Manifest
|
|
|
+import android.app.Dialog
|
|
|
+import android.bluetooth.BluetoothAdapter
|
|
|
+import android.bluetooth.BluetoothDevice
|
|
|
+import android.bluetooth.BluetoothManager
|
|
|
+import android.content.Context
|
|
|
+import android.content.DialogInterface
|
|
|
+import android.content.pm.PackageManager
|
|
|
+import android.os.Bundle
|
|
|
+import android.os.Handler
|
|
|
+import android.text.TextUtils
|
|
|
+import android.view.Gravity
|
|
|
+import android.view.LayoutInflater
|
|
|
+import android.view.View
|
|
|
+import android.view.ViewGroup
|
|
|
+import android.widget.*
|
|
|
+import androidx.appcompat.app.AlertDialog
|
|
|
+import androidx.core.app.ActivityCompat
|
|
|
+import androidx.core.content.ContextCompat
|
|
|
+import androidx.fragment.app.DialogFragment
|
|
|
+import com.android.chileaf.WearManager
|
|
|
+import com.android.chileaf.bluetooth.scanner.ScanCallback
|
|
|
+import com.android.chileaf.bluetooth.scanner.ScanResult
|
|
|
+import com.example.watch.R
|
|
|
+import com.example.watch.ui.activity.ScannerFragment.OnDeviceSelectedListener
|
|
|
+import timber.log.Timber
|
|
|
+import java.util.*
|
|
|
+
|
|
|
+/**
|
|
|
+ * ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter
|
|
|
+ * devices with standard BLE Service UUID and devices with custom BLE Service UUID. It contains a
|
|
|
+ * list and a button to scan/cancel. There is a interface [OnDeviceSelectedListener] which is
|
|
|
+ * implemented by activity in order to receive selected device. The scanning will continue to scan
|
|
|
+ * for 5 seconds and then stop.
|
|
|
+ */
|
|
|
+class ScannerFragment : DialogFragment() {
|
|
|
+ private var mBluetoothAdapter: BluetoothAdapter? = null
|
|
|
+ private var mListener: OnDeviceSelectedListener? = null
|
|
|
+ private var mAdapter: DeviceListAdapter? = null
|
|
|
+ private val mHandler = Handler()
|
|
|
+ private var mScanButton: Button? = null
|
|
|
+ private var mPermissionRationale: View? = null
|
|
|
+ private var mIsScanning = false
|
|
|
+
|
|
|
+ fun getInstance(): ScannerFragment? {
|
|
|
+ val fragment = ScannerFragment()
|
|
|
+ val args = Bundle()
|
|
|
+ fragment.arguments = args
|
|
|
+ return fragment
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Interface required to be implemented by activity.
|
|
|
+ */
|
|
|
+ interface OnDeviceSelectedListener {
|
|
|
+ /**
|
|
|
+ * Fired when user selected the device.
|
|
|
+ *
|
|
|
+ * @param device the device to connect to
|
|
|
+ * @param name the device name. Unfortunately on some devices [BluetoothDevice.getName]
|
|
|
+ * always returns `null`, i.e. Sony Xperia Z1 (C6903) with Android 4.3.
|
|
|
+ * The name has to be parsed manually form the Advertisement packet.
|
|
|
+ */
|
|
|
+ fun onDeviceSelected(device: BluetoothDevice?, name: String?)
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fired when scanner dialog has been cancelled without selecting a device.
|
|
|
+ */
|
|
|
+ fun onDialogCanceled() {}
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This will make sure that [OnDeviceSelectedListener] interface is implemented by activity.
|
|
|
+ */
|
|
|
+ override fun onAttach(context: Context) {
|
|
|
+ super.onAttach(context)
|
|
|
+ try {
|
|
|
+ mListener = context as OnDeviceSelectedListener
|
|
|
+ } catch (e: ClassCastException) {
|
|
|
+ throw ClassCastException("$context must implement OnDeviceSelectedListener")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
+ super.onCreate(savedInstanceState)
|
|
|
+ val manager =
|
|
|
+ requireContext().getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
|
+ if (manager != null) {
|
|
|
+ mBluetoothAdapter = manager.adapter
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onDestroyView() {
|
|
|
+ stopScan()
|
|
|
+ super.onDestroyView()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
|
+ val builder = AlertDialog.Builder(requireContext())
|
|
|
+ val dialogView: View =
|
|
|
+ LayoutInflater.from(activity).inflate(R.layout.fragment_device_scan, null)
|
|
|
+ val listview = dialogView.findViewById<ListView>(R.id.list)
|
|
|
+ listview.emptyView = dialogView.findViewById(R.id.empty)
|
|
|
+ listview.adapter = activity?.let {
|
|
|
+ DeviceListAdapter(it)
|
|
|
+ .also({ mAdapter = it })
|
|
|
+ }
|
|
|
+ builder.setTitle(R.string.scanner_title)
|
|
|
+ val dialog = builder.setView(dialogView).create()
|
|
|
+ listview.onItemClickListener =
|
|
|
+ AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long ->
|
|
|
+ stopScan()
|
|
|
+ dialog.dismiss()
|
|
|
+ val d: ExtendedBluetoothDevice =
|
|
|
+ mAdapter?.getItem(position) as ExtendedBluetoothDevice
|
|
|
+ mListener?.onDeviceSelected(d.device, d.name)
|
|
|
+ }
|
|
|
+ mPermissionRationale =
|
|
|
+ dialogView.findViewById(R.id.permission_rationale) // this is not null only on API23+
|
|
|
+ mScanButton = dialogView.findViewById(R.id.action_cancel)
|
|
|
+ mScanButton?.setOnClickListener(View.OnClickListener { v: View ->
|
|
|
+ if (v.id == R.id.action_cancel) {
|
|
|
+ if (mIsScanning) {
|
|
|
+ dialog.cancel()
|
|
|
+ } else {
|
|
|
+ startScan()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ addBoundDevices()
|
|
|
+ if (savedInstanceState == null) startScan()
|
|
|
+ return dialog
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCancel(dialog: DialogInterface) {
|
|
|
+ super.onCancel(dialog)
|
|
|
+ mListener?.onDialogCanceled()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onRequestPermissionsResult(
|
|
|
+ requestCode: Int,
|
|
|
+ permissions: Array<String>,
|
|
|
+ grantResults: IntArray
|
|
|
+ ) {
|
|
|
+ when (requestCode) {
|
|
|
+ REQUEST_PERMISSION_REQ_CODE -> {
|
|
|
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
|
+ // We have been granted the Manifest.permission.ACCESS_COARSE_LOCATION permission. Now we may proceed with scanning.
|
|
|
+ startScan()
|
|
|
+ } else {
|
|
|
+ mPermissionRationale!!.visibility = View.VISIBLE
|
|
|
+ Toast.makeText(activity, R.string.no, Toast.LENGTH_SHORT)
|
|
|
+ .show()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Scan for 5 seconds and then stop scanning when a BluetoothLE device is found then mLEScanCallback
|
|
|
+ * is activated This will perform regular scan for custom BLE Service UUID and then filter out.
|
|
|
+ * using class ScannerServiceParser
|
|
|
+ */
|
|
|
+ private fun startScan() {
|
|
|
+ // Since Android 6.0 we need to obtain either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for
|
|
|
+ // Bluetooth LE devices. This is related to beacons as proximity devices.
|
|
|
+ // On API older than Marshmallow the following code does nothing.
|
|
|
+ if (ContextCompat.checkSelfPermission(
|
|
|
+ requireContext(),
|
|
|
+ Manifest.permission.ACCESS_COARSE_LOCATION
|
|
|
+ ) != PackageManager.PERMISSION_GRANTED
|
|
|
+ ) {
|
|
|
+ // When user pressed Deny and still wants to use this functionality, show the rationale
|
|
|
+ if (ActivityCompat.shouldShowRequestPermissionRationale(
|
|
|
+ requireActivity(),
|
|
|
+ Manifest.permission.ACCESS_COARSE_LOCATION
|
|
|
+ ) && mPermissionRationale!!.visibility == View.GONE
|
|
|
+ ) {
|
|
|
+ mPermissionRationale!!.visibility = View.VISIBLE
|
|
|
+ return
|
|
|
+ }
|
|
|
+ requestPermissions(
|
|
|
+ arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
|
|
|
+ REQUEST_PERMISSION_REQ_CODE
|
|
|
+ )
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Hide the rationale message, we don't need it anymore.
|
|
|
+ if (mPermissionRationale != null) mPermissionRationale!!.visibility = View.GONE
|
|
|
+ mAdapter?.clearDevices()
|
|
|
+ mScanButton?.setText(R.string.scanner_action_cancel)
|
|
|
+ WearManager.getInstance(activity).startScan(scanCallback)
|
|
|
+ mIsScanning = true
|
|
|
+ mHandler.postDelayed({
|
|
|
+ if (mIsScanning) {
|
|
|
+ stopScan()
|
|
|
+ }
|
|
|
+ }, SCAN_DURATION)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Stop scan if user tap Cancel button
|
|
|
+ */
|
|
|
+ private fun stopScan() {
|
|
|
+ if (mIsScanning) {
|
|
|
+ mScanButton?.setText(R.string.scanner_action_scan)
|
|
|
+ WearManager.getInstance(activity).stopScan()
|
|
|
+ mIsScanning = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private val scanCallback: ScanCallback = object : ScanCallback() {
|
|
|
+ override fun onBatchScanResults(results: List<ScanResult>) {
|
|
|
+ mAdapter?.update(results)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun addBoundDevices() {
|
|
|
+ val devices = mBluetoothAdapter!!.bondedDevices
|
|
|
+ mAdapter?.addBondedDevices(devices)
|
|
|
+ }
|
|
|
+
|
|
|
+ private class DeviceListAdapter(private val mContext: Context) :
|
|
|
+ BaseAdapter() {
|
|
|
+ private val mListBondedValues: ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice> =
|
|
|
+ ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice>()
|
|
|
+ private val mListValues: ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice> =
|
|
|
+ ArrayList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice>()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets a list of bonded devices.
|
|
|
+ *
|
|
|
+ * @param devices list of bonded devices.
|
|
|
+ */
|
|
|
+ fun addBondedDevices(devices: Set<BluetoothDevice>) {
|
|
|
+ val bondedDevices: MutableList<com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice> =
|
|
|
+ mListBondedValues
|
|
|
+ for (device in devices) {
|
|
|
+ if (matchDeviceName(device.name)) {
|
|
|
+ bondedDevices.add(
|
|
|
+ com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice(
|
|
|
+ device
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ notifyDataSetChanged()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun matchDeviceName(name: String?): Boolean {
|
|
|
+ if (name != null && !TextUtils.isEmpty(name)) {
|
|
|
+ for (filterName in FILTER_NAMES) {
|
|
|
+ if (name.toUpperCase().startsWith(filterName)) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Updates the list of not bonded devices.
|
|
|
+ *
|
|
|
+ * @param results list of results from the scanner
|
|
|
+ */
|
|
|
+ fun update(results: List<ScanResult>) {
|
|
|
+ for (result in results) {
|
|
|
+ Timber.e(result.toString())
|
|
|
+ val device: com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice? =
|
|
|
+ findDevice(result)
|
|
|
+ if (device == null) {
|
|
|
+ if (hasPairAddress(result)) {
|
|
|
+ mListValues.add(
|
|
|
+ com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice(
|
|
|
+ result
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } else if (result.scanRecord != null) {
|
|
|
+ device.name = result.scanRecord!!.deviceName
|
|
|
+ device.rssi = result.rssi
|
|
|
+ }
|
|
|
+ }
|
|
|
+ notifyDataSetChanged()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun hasPairAddress(result: ScanResult): Boolean {
|
|
|
+ val record = result.scanRecord
|
|
|
+ val name = if (record != null) record.deviceName else ""
|
|
|
+ return matchDeviceName(name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun findDevice(result: ScanResult): com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice? {
|
|
|
+ for (device in mListBondedValues) if (device.matches(result) && hasPairAddress(result)) return device
|
|
|
+ for (device in mListValues) if (device.matches(result)) return device
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ fun clearDevices() {
|
|
|
+ mListValues.clear()
|
|
|
+ notifyDataSetChanged()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getCount(): Int {
|
|
|
+ val bondedCount = mListBondedValues.size + 1 // 1 for the title
|
|
|
+ val availableCount =
|
|
|
+ if (mListValues.isEmpty()) 2 else mListValues.size + 1 // 1 for title, 1 for empty text
|
|
|
+ return if (bondedCount == 1) availableCount else bondedCount + availableCount
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getItem(position: Int): Any {
|
|
|
+ val bondedCount = mListBondedValues.size + 1 // 1 for the title
|
|
|
+ return if (mListBondedValues.isEmpty()) {
|
|
|
+ if (position == 0) R.string.scanner_subtitle_not_bonded else mListValues[position - 1]
|
|
|
+ } else {
|
|
|
+ if (position == 0) return R.string.scanner_subtitle_bonded
|
|
|
+ if (position < bondedCount) return mListBondedValues[position - 1]
|
|
|
+ if (position == bondedCount) R.string.scanner_subtitle_not_bonded else mListValues[position - bondedCount - 1]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getViewTypeCount(): Int {
|
|
|
+ return 3
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun areAllItemsEnabled(): Boolean {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun isEnabled(position: Int): Boolean {
|
|
|
+ return getItemViewType(position) == com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_ITEM
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getItemViewType(position: Int): Int {
|
|
|
+ if (position == 0) return com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_TITLE
|
|
|
+ if (!mListBondedValues.isEmpty() && position == mListBondedValues.size + 1) return com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_TITLE
|
|
|
+ return if (position == count - 1 && mListValues.isEmpty()) com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_EMPTY else com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_ITEM
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getItemId(position: Int): Long {
|
|
|
+ return position.toLong()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getView(position: Int, oldView: View, parent: ViewGroup): View {
|
|
|
+ val inflater = LayoutInflater.from(mContext)
|
|
|
+ val type = getItemViewType(position)
|
|
|
+ var view = oldView
|
|
|
+ when (type) {
|
|
|
+ com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_EMPTY -> if (view == null) {
|
|
|
+ view = TextView(mContext)
|
|
|
+ val empty = view
|
|
|
+ empty.gravity = Gravity.CENTER_HORIZONTAL
|
|
|
+ empty.text = mContext.getString(R.string.scanner_empty)
|
|
|
+ }
|
|
|
+ com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.Companion.TYPE_TITLE -> {
|
|
|
+ if (view == null) {
|
|
|
+ view = TextView(mContext)
|
|
|
+ }
|
|
|
+ val title = view as TextView
|
|
|
+ title.gravity = Gravity.CENTER_HORIZONTAL
|
|
|
+ title.setText((getItem(position) as Int))
|
|
|
+ }
|
|
|
+ else -> {
|
|
|
+// if (view == null) {
|
|
|
+// view = inflater.inflate(R.layout.item_device_list, parent, false)
|
|
|
+// val holder: com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder =
|
|
|
+// com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder()
|
|
|
+//
|
|
|
+// holder.name = view.findViewById<TextView>(R.id.name)
|
|
|
+// holder.address = view.findViewById<TextView>(R.id.address)
|
|
|
+// holder.signal = view.findViewById<TextView>(R.id.rssi)
|
|
|
+// view.tag = holder
|
|
|
+// }
|
|
|
+ val device: com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice =
|
|
|
+ getItem(position) as com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice
|
|
|
+ val holder: com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder =
|
|
|
+ view.tag as com.example.watch.ui.activity.ScannerFragment.DeviceListAdapter.ViewHolder
|
|
|
+ val name: String? = device.name
|
|
|
+ holder.name?.setText(name ?: mContext.getString(R.string.not_available))
|
|
|
+ holder.address?.setText(device.device.getAddress())
|
|
|
+ if (!device.isBonded || device.rssi != com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice.Companion.NO_RSSI) {
|
|
|
+ holder.signal?.setText(device.rssi.toString() + "dBm")
|
|
|
+ holder.signal?.setVisibility(View.VISIBLE)
|
|
|
+ } else {
|
|
|
+ holder.signal?.setVisibility(View.GONE)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return view
|
|
|
+ }
|
|
|
+
|
|
|
+ private inner class ViewHolder {
|
|
|
+ var name: TextView? = null
|
|
|
+ var address: TextView? = null
|
|
|
+ var signal: TextView? = null
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ private const val TYPE_TITLE = 0
|
|
|
+ private const val TYPE_ITEM = 1
|
|
|
+ private const val TYPE_EMPTY = 2
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private class ExtendedBluetoothDevice {
|
|
|
+ var name: String?
|
|
|
+ var rssi: Int
|
|
|
+ var isBonded: Boolean
|
|
|
+ val device: BluetoothDevice
|
|
|
+
|
|
|
+ constructor(scanResult: ScanResult) {
|
|
|
+ device = scanResult.device
|
|
|
+ name = if (scanResult.scanRecord != null) scanResult.scanRecord!!.deviceName else null
|
|
|
+ rssi = scanResult.rssi
|
|
|
+ isBonded = false
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor(device: BluetoothDevice) {
|
|
|
+ this.device = device
|
|
|
+ name = device.name
|
|
|
+ rssi =
|
|
|
+ com.example.watch.ui.activity.ScannerFragment.ExtendedBluetoothDevice.Companion.NO_RSSI
|
|
|
+ isBonded = true
|
|
|
+ }
|
|
|
+
|
|
|
+ fun matches(scanResult: ScanResult): Boolean {
|
|
|
+ return device.address == scanResult.device.address
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ const val NO_RSSI = -1000
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ @JvmName("getInstance1")
|
|
|
+ fun getInstance(): ScannerFragment {
|
|
|
+ val fragment = ScannerFragment()
|
|
|
+ val args = Bundle()
|
|
|
+ fragment.arguments = args
|
|
|
+ return fragment
|
|
|
+ }
|
|
|
+
|
|
|
+ private const val SCAN_DURATION: Long = 5000
|
|
|
+ private const val REQUEST_PERMISSION_REQ_CODE = 34
|
|
|
+ private val FILTER_NAMES = arrayOf("CL831", "SE2")
|
|
|
+ val instance: ScannerFragment
|
|
|
+ get() {
|
|
|
+ val fragment = ScannerFragment()
|
|
|
+ val args = Bundle()
|
|
|
+ fragment.arguments = args
|
|
|
+ return fragment
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|