Android Broadcast Receiver 教學

Photo by Roman Kraft on Unsplash
Photo by Roman Kraft on Unsplash
Android Broadcast Receiver 元件讓 app 可以從 Android 系統或其他 apps 接收訊息,也可以傳送訊息給 app 自己的其他元件,或是其他 apps。它類似於 publish-subscribe 設計模式。

Android Broadcast Receiver 元件讓 app 可以從 Android 系統或其他 apps 接收訊息,也可以傳送訊息給 app 自己的其他元件,或是其他 apps。它類似於 publish-subscribe 設計模式。本文章將介紹如何使用 Broadcast Receiver。

完整程式碼可以在 下載。

概覽

當有系統事件發生時,Android 系統自動地廣播事件給所有監聽該事件的 apps。例如,當使用者開啟或關係飛航模式時,系統會廣播 ACTION_AIRPLANE_MODE_CHANGED 事件。

App 也可以廣播事件給所有監聽該事件的 apps。當然 App 自己也可以監聽自己廣播的事件。

接收 Broadcasts

App 有兩種方式可以接收 broadcasts,一種是註冊 manifest-declared receivers,另一種是註冊 context-declared receivers。

Manifest-Declared Receivers

Manifest-declared receivers 指的是在 AndroidManifest.xml 中註冊的 receivers。在 <receiver/> 中設定監聽事件的 class,並且在 <intent-filter/> 中指定要監聽的事件。如果事件來源是系統或是其他的 apps,還要設定 android:exported="true"

以下範例中,我們建立 LocaleChangedBroadcastReceiver 並繼承 BroadcastReceiver,然後 override onReceive() 即可。如果事件帶有額外資料的話,我們可以由 intent 中取得。

package com.waynestalk.broadcastreceiverexample

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

class LocaleChangedBroadcastReceiver : BroadcastReceiver() {
    companion object {
        private const val TAG = "LocaleChangedBroadcast"
    }

    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "Received: locale changed => ${Thread.currentThread()}")
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidBroadcastReceiverExample"
        tools:targetApi="31">

        <receiver
            android:name=".LocaleChangedBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.LOCALE_CHANGED" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

Android 提供很多系統事件,請參照 Intent constants。在 Android 8(API level 26)以上,app 不能透過 manifest-declared receivers 來接收 implicit broadcasts。Implicit broadcasts 是那些沒有指定你的 app 為目的地的 broadcasts。例如,系統事件就是 implicit broadcasts。所以你不能用 manifest-declared receivers 來接收系統事件,只有一些少數的 implicit broadcasts 除外。

當 app 從沒有被使用者執行或是被 force stop 的話,manifest-declared receivers 不會接收到事件。

如下面第一個圖,如果 app 正在執行,或是在背景,或是被使用者從背景移除,在 App info 中,Force stop 按鈕是可以按的。這表示 app 還是啟動的。這時 app 的 manifest-declared receivers 可以接收到事件。

App is launched.
App is launched.

如果使用者按下 Force stop 來停止 app,那 app 的 manifest-declared receivers 就不會再接收到事件了。當系統啟動後,如果使用者從未執行過 app,那 app 也是停止的。在某些廠牌的 Android 系統中,當使用者將 app 從背景移除時,系統會將 app force stop。

App is not launched, or force-stopped.
App is not launched, or force-stopped.

Context-Declared Receivers

Context-declared receivers 指的是在 Android 元件中,透過 Context.registerReceiver() 或是 ContextCompat.registerReceiver() 註冊的 receivers。

如下範例,我們在 MainActivity.onResume() 中註冊 AirplaneModeBroadcastReceiver。因為它監聽的是來自系統的事件,所以要設定 ContextCompat.RECEIVER_EXPORTED flag。最後,我們還要在 MainActivity.onPause() 中呼叫 Context.unregisterReceiver() 來取消註冊,以防止註冊多次。

package com.waynestalk.broadcastreceiverexample

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class AirplaneModeBroadcastReceiver : BroadcastReceiver() {
    companion object {
        private const val TAG = "AirplaneModeBroadcast"
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != Intent.ACTION_AIRPLANE_MODE_CHANGED) return

        val isOn = intent.getBooleanExtra("state", false)
        val message = "Airplane mode is ${if (isOn) "on" else "off"}."
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
}
package com.waynestalk.broadcastreceiverexample

import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.waynestalk.broadcastreceiverexample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private val broadcastReceiver = AirplaneModeBroadcastReceiver()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    override fun onResume() {
        super.onResume()

        val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
        val flags = ContextCompat.RECEIVER_EXPORTED
        ContextCompat.registerReceiver(this, broadcastReceiver, filter, flags)
    }

    override fun onPause() {
        super.onPause()

        unregisterReceiver(broadcastReceiver)
    }
}

Lifecycle

當系統傳送事件給 app 時,app 內監聽該事件的 BroadcastReceiver 就會被建立,並且系統會在該 process 中 main thread 中執行 BroadcastReceiver.onReceive()。當 onReceive() 執行完畢後,BroadcastReceiver 就不再是 active。系統會每次都會建立一個新的 BroadcastReceiver 來處理新的事件,所以當 onReceive() 執行完畢後,該 BroadcastReceiver 的生命週期算是結束了。

當 onReceive() 被執行時,其 hosting process 會被系統視為 foreground process。所以即使系統遭遇到 low memory 的情況時,也不會結束 app。但是當 onReceive() 結束後,如果 app 是 background process 的話,那系統可能會結束 process。

所以在 onReceive() 內,我們不應該建立 long running thread。因為當 onReceive() 一結束時,系統可能會結束 process,也會結束該 process 建立的 threads。

我們也不應該在 onReceive() 中執行 long running 的操作,因為 onReceive() 是在 main thread 中執行的。當使用者正在使用 app,而剛好 onReceive() 被執行來處理某個事件時,如果 onReceive() 花太多時間在 main thread 中處理事件的話,那對使用者體驗是不好的。

如果必須要在 onReceive() 處理 long running 的操作的話,可以用 JobSchedulerJobService,或是 BroadcastReceiver.goAsync()。goAsync() 告知系統需要更多的時間來處理事件(上限為10秒)。如下範例中的 onReceive(),它先呼叫 goAsync(),然後在 coroutine 中執行 long running 操作,這樣就不會 block main thread。最後要呼叫。 PendingResult.finish() 來告知系統處理結束了。

class MyBroadcastReceiver : BroadcastReceiver() {
    private val scope = CoroutineScope(SupervisorJob())

    override fun onReceive(context: Context, intent: Intent) {
        val pendingResult: PendingResult = goAsync()

        scope.launch(Dispatchers.Default) {
            // ...

            // Must call finish() so the BroadcastReceiver can be recycled
            pending.finish()
        }
    }
}

傳送 Broadcasts

App 可以呼叫 sendBroadcast(Intent, String) 廣播事件給自己和其他的 apps。

如下面的範例中,EchoBroadcastReceiver 監聽 com.waynestalk.echo 事件。我們在 AndroidManifest.xml 中註冊它。在 MainActivity 中,建立一個 intent,並設定要廣播 com.waynestalk.echo 事件。在 Android 8(API level 26)以上,app 不能透過 manifest-declared receivers 來接收 implicit broadcasts,所以我們在 Intent.package 設定接收者的 application package,也就是這個事件的目的地。因為是自己廣播給自己,所以就設定自己的 application package。

package com.waynestalk.broadcastreceiverexample

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class EchoBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != "com.waynestalk.echo") return

        val message = intent.getStringExtra("message")
        Toast.makeText(context, "Received: $message", Toast.LENGTH_LONG).show()
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidBroadcastReceiverExample"
        tools:targetApi="31">

        <receiver
            android:name=".EchoBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.waynestalk.echo" />
            </intent-filter>
        </receiver>
    </application>

</manifest>
package com.waynestalk.broadcastreceiverexample

import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.waynestalk.broadcastreceiverexample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.echoButton.setOnClickListener {
            val intent = Intent().apply {
                action = "com.waynestalk.echo"
                `package` = "com.waynestalk.broadcastreceiverexample"
                putExtra("message", "Hello Android")
            }
            sendBroadcast(intent)
        }
    }
}

結語

系統事件讓 app 可以做出合適的反應,列如 app 可以監聽網路連線的改變。如果是斷線的話,app 可以顯示訊息請求使用者開啟網路連線。而且 app 不需要一直 pull 連線狀態,而是用監聽系統事件來得知網路狀態是否改變。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More