ブロードキャストでAndroidのシステム情報を取得する

# ブロードキャストとは
[公式ドキュメント](https://developer.android.com/guide/components/broadcasts?hl=ja)には以下のように書かれています。
> たとえば、システムの起動、デバイスの充電の開始など、さまざまなシステム イベントが発生したときに、Android システムがブロードキャストを送信します。
>
このようにブロードキャストを用いることで、Androidシステムの情報を取得することができるようになります。

# システム情報を取得する際の注意点
プライバシーの問題からシステム情報を取得する方法は刻一刻と変化しています。
特にAndroidのアップデートの際は、取得する方法が変わっていないか注意しましょう。

## OSのバージョンによる制限
OSのバージョンによって取得できる情報が制限されるようになっています。

#### Android9以降
Android9以降のデバイスでは、プライバシーの観点から以下の情報が取得できなくなっています。
> - 位置情報や個人を特定できるデータを受信しなくなっています。
> - Wi-Fi からのシステム ブロードキャストに SSID、BSSID、接続情報、スキャン結果は含まれません。
>
#### Android8以降
Android8以降のデバイスでは、ブロードキャストのレシーバーを宣言する方法が制限されています。マニフェストを使用するブロードキャストの記述は避けましょう。（ただし[例外](https://developer.android.com/guide/components/broadcast-exceptions?hl=ja)もあります）
> システムはマニフェストで宣言されたレシーバーに対して追加の制限を課します。

# 実装
それでは実装方法を説明します。

上でも説明しましたが、マニフェストを使用するブロードキャストの記述ができなくなっているため、コンテキストを用いた方法を紹介します。

## 受信方法
### レシーバーの作成
まず、`BroadcastReceiver`を継承したファイルを作成し、`onReceive`を`override`します。
その後、`onReceive`内で受信したいイベントを定義し、イベントが起こったときにする動作を書きます。
```kotlin:Receiver
class TimeZoneBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // 受信したいイベントを定義
        if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
            // イベントが起こったときに発生する動作を定義する
            // 動作が完了しない場合があるので注意（下で説明します）
        }
    }
}
```
コールバックを用いて以下のように書くこともできます。
```kotlin: Receiver(callback)
private val br = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        // 受信したいイベントを定義
        if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
            // イベントが起こったときに発生する動作を定義する
            // 動作が完了しない場合があるので注意（下で説明します）
        }
    }
}
```

### レシーバーを登録
次に、定義したレシーバーを登録します。以下の3つを行います。
- 登録、解除を定義する
- どのイベントを受け取るか
- いつからいつまで観測するのか

まず、登録、解除を定義します。
また、登録の際にレシーバーがどのイベントを受信するのかを定義します。
```kotlin: register function
fun register(context: Context) {
    if (!registered) {
        val filter = IntentFilter()
        // どのイベントを受け取るのかを定義
        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
        context.registerReceiver(this, filter)
        registered = true
    }
}
```
```kotlin: unregister function
fun unregister(context: Context) {
    if (registered) {
        context.unregisterReceiver(this)
        registered = false
    }
}
```

次に、いつから観測するのかを教えてあげます。

アプリが起動してから終了するまで観測したい場合は、Activityのようなアプリが終了するまで生存しているファイルの**onCreate**で登録し、**onDestroy**で登録解除を行なってあげましょう。
```kotlin:MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
    val receiver = TimeZoneBroadcastReceiver()
    receiver.register(context)
}

override fun onDestroy() {
    receiver.unregister(context)
}
```

## イベントが起こったときにする動作について
イベントが起こって動作を行う際、OSが優先度の低いタスクだと判断し、動作が強制終了される可能性があります。
[公式ドキュメント](https://developer.android.com/guide/components/broadcasts?hl=ja#effects-process-state)にも以下のように記載されています。
> onReceive() からコードが返されると、システムは対象のプロセスを優先度の低いプロセスとみなし、重要度の高い他のプロセスでリソースを利用できるようにするため、このプロセスを強制終了する可能性があります。
>
それゆえ、OSに処理時間が必要であることを伝える必要があります。

### OSに処理時間を伝える方法
さまざまな方法がありますが、ここでは2つ紹介します。
- `goAsync()`
- flowの`awaitClose`を用いる

#### `goAsync()`
[公式ドキュメント](https://developer.android.com/guide/components/broadcasts?hl=ja#effects-process-state)で紹介されている方法です。

以下のように処理時間を伝えてあげる必要があります。
- onReceive内で`goAsync`のインスタンスを作成する
- 行いたい処理をAsyncTaskを継承したクラス内で定義する
- `execute()`で開始する
- onPostExecute内の`finish()`で終了する
```kotlin:goAsyncを用いて処理時間が必要なことを伝える
override fun onReceive(context: Context, intent: Intent) {
    val pendingResult: PendingResult = goAsync()
    val asyncTask = Task(pendingResult, intent)
    // 開始を伝える
    asyncTask.execute()
}

// 行いたい処理のクラス
private class Task(
        private val pendingResult: PendingResult,
        private val intent: Intent
) : AsyncTask<String, Int, String>() {

    override fun doInBackground(vararg params: String?): String {
        // 行いたい処理を書く
    }

    override fun onPostExecute(result: String?) {
        super.onPostExecute(result)
        // 終了を伝える
        pendingResult.finish()
    }
}
```

#### flowの`awaitClose`を用いる
flowを用いている場合はこちらを使うとスッキリと書くことができます。
[nowinandroid](https://github.com/android/nowinandroid/blob/main/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt)でも使われている方法となっています。

```kotlin:awaitClose
override val isOnline: Flow<Boolean> = callbackFlow {

    val callback = object : BroadcastReceiver() {
       override fun onReceive(context: Context?, intent: Intent?) {
        // 受信したいイベントを定義
            if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
                // イベントが起こったときに発生する動作を定義する
                channel.trySend(true)
            }
        }
        channel.trySend(connectivityManager.isCurrentlyConnected())

        // onReceive内の処理がキャンセルされないように、受信するまではスレッドをブロックする
        awaitClose()
    }
```

awaitCloseは[スタゼロさんのブログ記事](https://star-zero.medium.com/%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E9%80%9A%E7%9F%A5%E3%82%92flow%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B-5ba3760c4264)によると次のような機能を持っているそうです。`callbackFlow`の中で呼び出すことができ、callbackFlowが終了するまで他の処理をブロックする役割を持っています。
> Flow Channelがクローズまたはキャンセルされるまで処理をブロックします
>
flowの導入を検討している際はこちらの方法を用いると便利に使うことができます。

## まとめ
- ブロードキャストによってAndroidのシステム情報を取得できる
- AndroidのOSによって取得できる情報は変わる
- レシーバーを定義、登録することで用いることができる
- OSに処理時間を伝える必要がある

## 最後に

最後まで読んでいただきありがとうございました。よければtwitterのフォローよろしくお願いします。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">⚡️ &quot;たかっしーの開発日記&quot;<a href="https://t.co/aGE0WGERzU">https://t.co/aGE0WGERzU</a></p>&mdash; たかっしー（開発垢）@東北放浪中 (@takashiho_2) <a href="https://twitter.com/takashiho_2/status/1592537525981450240?ref_src=twsrc%5Etfw">November 15, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>