Android BLE ドキュメントを参考にSCANまで

Android BLE を下記、ドキュメントを参考に実装します。
今回は、Scanした結果をログ出力するところまでやります。
曖昧な理解なところがあるのであくまで参考程度で。
①概要
https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja
②Bluetooth Low Energyの概要
https://developer.android.com/guide/topics/connectivity/bluetooth-le?hl=ja#find
③Bluetooth Low Energyを使用する
https://developer.android.com/guide/topics/connectivity/use-ble?hl=ja#connect

#BLEパーミッション
②Bluetooth Low Energyの概要に記載があるように、BluetoothとLocationのパーミションが必要となります。
>ビーコンは位置情報に関連付けられていることが多いため、ACCESS_FINE_LOCATION パーミッションも宣言する必要があります。このパーミッションがないと、スキャン結果を取得できません。

```kotlin:AndroidManifest.xml
uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<!-- If your app targets Android 9 or lower, you can declare
     ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
```
実際はRuntime permissionを使って許可を求めると思いますが、今回は割愛します。
設定から、手動で許可をして下さい。

#Bluetooth有効か確認
Bluetoothが有効かどうか確認するメソッドです。
③Bluetooth Low Energyを使用するを参考にします。

BluetoothAdapterを取得する。
下記、ドキュメント通りBluetoothAdapterを取得しようとするとエラーになる。

```kotlin
// Initializes Bluetooth adapter.
val bluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager?.adapter
```
bluetoothManagerがNullとなりエラーとなる。

>Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getSystemServiceName(java.lang.Class)' on a null object reference

なので下記のように、変更する。（①概要を参考にした。）

```kotlin
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
```

Bluetoothを有効にするメソッド。有効でない場合は、ダイヤログを表示する。
REQUEST_ENABLE_BTは何でもよいので、定数を宣言する。

```kotlin
private val REQUEST_ENABLE_BT = 1
```

```kotlin
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (bluetoothAdapter != null && !bluetoothAdapter.isEnabled) {
   val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
   startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619313/8a56b3d3-282b-ed6d-d4bc-b5e7a354bfe6.png)

#SCAN
③Bluetooth Low Energyを使用するを参考にします。

bluetoothLeScannerクラスを使用します。
SCAN_PERIODはSCAN時間になります。

```kotlin
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
private var scanning = false
private val handler = Handler()

// Stops scanning after 10 seconds.
private val SCAN_PERIOD: Long = 10000
```

SCANメソッドは下記になります。
コールバックのleScanCallbackを実装する必要があります。

```kotlin
private fun scanLeDevice() {
    bluetoothLeScanner?.let { scanner ->
        if (!scanning) { // Stops scanning after a pre-defined scan period.
            handler.postDelayed({
                scanning = false
                scanner.stopScan(leScanCallback)
            }, SCAN_PERIOD)
            scanning = true
            scanner.startScan(leScanCallback)
        } else {
            scanning = false
            scanner.stopScan(leScanCallback)
        }
    }
```

コールバック部

```kotlin
private val leScanCallback: ScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        super.onScanResult(callbackType, result)
        //コメントアウト部、今回は不要（別途実装が必要になる）
        //leDeviceListAdapter.addDevice(result.device)
        //leDeviceListAdapter.notifyDataSetChanged()

        //nameをログで出力する。nullだった場合No Name
            Log.d("result.device.name",result.device.name ?: "No Name")
    }
}
```
ドキュメントでは下記を実装している。推測するに、SCANして見つかった情報をListに追加したいのかな。
別途実装が必要で、今回はログ出力したいのでコメントアウト。
>leDeviceListAdapter.addDevice(result.device)
        leDeviceListAdapter.notifyDataSetChanged()

#全容
Bluetooth確認ボタンとSCANボタンを設置して、動作確認出来るようにしました。
SCANするには、位置情報の権限を許可にする必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619313/1dab9cb5-7805-eda8-6f2f-cff53c4dd884.png)

SCANして、ログ出力されればOKです。

```kotlin:MainActivity.kt
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.widget.Button

class MainActivity : AppCompatActivity() {

    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
    private var scanning = false
    private val handler = Handler()

    private val REQUEST_ENABLE_BT = 1
    private val SCAN_PERIOD: Long = 10000


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.bluetooth_check_button).setOnClickListener {
            if (bluetoothAdapter != null && !bluetoothAdapter.isEnabled) {
                val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
            }
        }

        findViewById<Button>(R.id.scan_button).setOnClickListener {
            scanLeDevice()
        }
    }

    private fun scanLeDevice() {
        bluetoothLeScanner?.let { scanner ->
            if (!scanning) { // Stops scanning after a pre-defined scan period.
                handler.postDelayed({
                    scanning = false
                    scanner.stopScan(leScanCallback)
                    println("stopScan")
                }, SCAN_PERIOD)
                scanning = true
                scanner.startScan(leScanCallback)
                println("startScan")
            } else {
                scanning = false
                scanner.stopScan(leScanCallback)
                println("stopScan")
            }
        }
    }

    private val leScanCallback: ScanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)

            //nameをログで出力する。nullだった場合No Name
            Log.d("result.device.name", result.device.name ?: "No Name")
        }
    }
}
```

```kotlin:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bluetooth_check_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:text="Bluetooth確認"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/scan_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:text="SCAN"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

#おわりに
BLEを触ってみたく、ドキュメントを見ながら実装をしました。
次は、GATTサーバーに接続からやってみようかなと。
あと、セントラルとペリフェラルが分からなく調べたところ、下記サイトが参考になりました。
https://houwa-js.co.jp/exe/2018/06/20180629/