第十九章 BroadcastReceiver

 何謂廣播

廣播, 就如同在學校, 當廣播XXX請到訓導處報到, 此時每一個學生都會聽到這個廣播, 但實際上只有XXX這一個人需要去報到.

廣播會在Android系統內流通, 很多程式都會接收到廣播, 但是會處理該廣播的訊息只會有某幾個程式而已.

BroadcastReceiver分成二個部份, 一個是發送廣播, 一個是接收廣播.

發送廣播的流程, 同樣是使用Intent, 並置入Action Name, 只是傳送Intent時, 不是用startActivity, 而是使用sendBroadcast(). 程式碼如下

在下面的BROADCAST字串, 可以為任何獨一無二的字串, 且必需與AndroidManifest.xml裏的intent-filter 一模一樣. 

請注意, 在M版以上, 需加 intent.setPackage(getPackageName()); 指定可以接收的Package. 也就是說, 不是任何的apk 都可以接收此廣播的. 

或者是使用intent.setComponent()指定可以接收的Receiver. 在setComponent()裏的第一個參數是Package名稱,  如com.asuscomm.mahaljsp, 第二個參數是 com.asuscomm.mahaljsp.MyReceiver. 不過第二個參數為什麼連網域都要再重打一次, 本人實在無法理解Google的神邏輯. 只是感覺Google 開始在走腐敗路線了.

public class MainActivity extends AppCompatActivity {
    Button btn;
    public static final String BROADCAST="net.ddns.mahaljsp.Broadcast1";
    public static Context context;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context=this;
        btn=(Button)findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent=new Intent(BROADCAST);
                intent.putExtra("name", "訓導處公告");
                intent.putExtra("message", "王小明請到訓導處報到");
                
                //M版以上必需加setPackage, 指定能接收的Package
                intent.setPackage(getPackageName());

                //或者加setComponent指定能接收的Receiver
                intent.setComponent(new ComponentName(getPackageName(), getPackageName()+".MyReceiver"));
                sendBroadcast(intent);
            }
        });
    }
}

而接收端需繼承BroadcastReceiver類別, 實作onReceiver()方法, 然後需於設定檔加入<receiver>標簽並設定要接收的Intent

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        AlertDialog.Builder builder=new AlertDialog.Builder(MainActivity.context);
        builder.setMessage(intent.getStringExtra("message"))
                .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {}
                })
                .setTitle(intent.getStringExtra("name"));
        builder.show();
    }
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.ddns.mahaljsp.ch19_01_broadcastreceiver">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".MyReceiver">
            <intent-filter>
                <action android:name="net.ddns.mahaljsp.Broadcast1" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

靜態註冊及動態註冊

在大陸用語上, 又稱為冷註冊及熱註冊. 將BroadcastReceiver寫在設定檔中, 稱為靜態(冷)註冊. 另一種方式是寫在程式碼中, 稱為動態(熱)註冊.

靜態註冊其真實的用意在於讓系統自動new 出BroadcastReceiver物件, 並註冊監聽且過濾Intent. 所以就算關閉程式, 甚至重新開機, 還是會持續的監聽.

動態註冊是在Activity的onResum()中下達registerReceiver()後才會開始監聽, 而在onPause()中需再下達unregisterReceiver()解除監聽

動態註冊方法 

public class MainActivity extends AppCompatActivity {
    BroadcastReceiver receiver=new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if ("net.ddns.mahaljsp.Broadcast2".equals(intent.getAction())) {
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle("接收端")
                        .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {}
                        })
                        .setMessage(intent.getStringExtra("message"))
                        .show();
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    protected void onResume() {
        super.onResume();
        registerReceiver(receiver, new IntentFilter("net.ddns.mahaljsp.Broadcast2"));
    }
    @Override
    protected void onPause() {
        unregisterReceiver(receiver);
        super.onPause();
    }
    public void btn_click(View view){
        Intent intent=new Intent("net.ddns.mahaljsp.Broadcast2");
        intent.putExtra("message","王小明請到訓導處報到");
        sendBroadcast(intent);
    }
}

Activity, Service, BroadcastReceiver

請注意, 下面說明, 已不適用在Android 8.0以上了

在Android 3.1及以上, BroadcastReceiver是無法單獨存在的. 也就是說一定要伴隨著一個Activity. 因為沒有Activity的行程是一個死行程, 不可以接收重要的廣播資訊.
所以如果不想由Activity來啟動Receiver的話, 則只能在Activity的onCreate()中取消調用setContentView(), 並立即使用finish()結束Activity. (若是小米手機,記得還要由安全中心設定自啟動管理才可接收BOOT_COMPLETED訊息)

而Service是可以單獨存在的, 但他無法自行啟動, 需透過Activity or BroadcastReceiver來啟動. 若Service單獨寫成一個Package時, 則在Activity或BroadcastReceiver的啟動方式需為顯示啟動, 如下所示

Intent i=new Intent();
i.setAction("net.ddns.mahaljsp.MyService");
i.setPackage("net.ddns.mahaljsp.phoneservice");
context.startService(i);

系統廣播事件

Android規畫許多系統廣播事件, 可供廣播接收程式所接收

ACTION_BOOT_COMPLETED : 開機完成訊號, android.intent.action.BOOT_COMPLETED
ACTION_STATE_CHANGED : 藍芽設備狀態改變
ACTION_NEW_VIDEO/ACTION_NEW_PICTURE : 相機影片與拍攝照片廣播事件
NETWORK_STATK_CHANGED_ACTION : 網路狀態改變
ACTION_TIME_CLICK : 每分鐘固定發送一次
ACTION_DATE_CHANGED/ACTION_TIME_CHANGED : 系統日期與時間變更

BOOT_COMPLETED

在裝置開機完後, 會發送開機廣播BOOT_COMPLETED訊息, 讓開機後需要自動執行的程式能立即啟動. 開機自動執行的程式需要寫一個BroadcastReceiver來接收此廣播, 再由BroadcastReceiver來啟動Activity或Service.

底下程式碼說明了開機完成後, 會自動執行一個Service,  而此Service會使用Toast印出1-10

BootService專案

先開啟一個專案, 名為Ch19_BootService, 在選擇Activity時選用Add no Activity. 然後新增BootService 類別. 因為此Service沒有Activity, 所以在工具列的app會有一個x, 點選app按鈕後, 再選Edit Configurations, 將Launch改為nothing

Ch19_BootService.java

public class BootService extends Service {
    MyTask task;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public void onCreate() {
        super.onCreate();
        task=new MyTask();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        task.execute();
        return Service.START_STICKY;
    }
    public class MyTask extends AsyncTask<Void, Integer, Void>{
        @Override
        protected Void doInBackground(Void... voids) {
            for (int i=1;i<=10;i++){
                publishProgress(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
        @Override
        protected void onProgressUpdate(Integer... values) {
            Toast.makeText(getApplicationContext(), "Service : "+values[0], Toast.LENGTH_LONG).show();
        }
    }
}

然後於設定檔中, 設定如下

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.ddns.mahaljsp.ch19_bootservice">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
    <service android:name=".BootService">
        <intent-filter>
            <action android:name="net.ddns.mahaljsp.BootService" />
        </intent-filter>
    </service>
    </application>
</manifest>

BootReceiver專案

接收廣播的程式碼, 先新增另一個專案, 名為Ch19_BootReceiver, 再新增一個BootReceiver Class. 此類別繼承BroadcastReceiver, 再由onReceiver()方法啟動Service. 因為Service是在另一個Package中, 所以需使用顯示Intent來啟動

BootReceiver.java

public class BootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "開機了", Toast.LENGTH_LONG).show();
        Intent intentService=new Intent();
        intentService.setAction("net.ddns.mahaljsp.BootService");
        intentService.setPackage("net.ddns.mahaljsp.ch19_bootservice");
        context.startService(intentService);
    }
}

至於MainActivity.java中, 因為不想看到UI畫面, 所以作如下變更

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        finish();
    }
}

然後於此專案的設定檔中, 需設定RECEIVE_BOOT_COMPLETED的權限, 另外再加入<receiver>的標簽及filter. 原始碼變更如下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.ddns.mahaljsp.ch19_bootreceiver">
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".BootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>

安裝及設定

將上述二支程式安裝到手機上. 請注意, 在小米手機上, 己屏蔽了此開機廣播, 若要取得此訊息, 需要由安全中心這支apk來設定. 進入安全中心後, 點選授權管理/自啟動管理, 然後把Ch19_BootReceiver這支應用程式的自啟開關打開, 如此才可以取得BootCompleted訊息.
最後重新開機, 即可看到Toast的訊息

完成程式碼下載 :  Ch19_BootReceiver.rar     Ch19_BootService.rar

最保險的作法

以下的作法, 是會在自動開機後, 自動啟動BroadcastReceiver, 然後再啟動Activity, 再由Activity啟動Service, 此法適用於Android 8.0以上的版本.

只不過此法會於開機後, 突然閃了一下, 然後才開始執行Service

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.asuscomm.mahaljsp.permissiontest">
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"></service>
        <receiver
            android:name=".MyReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>

MyReceiver.java

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent i=new Intent(context, MainActivity.class);
        context.startActivity((i));
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_PHONE_STATE_PERMISSION=100;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent=new Intent(this, MyService.class);
        startService(intent);
        finish();
        //setContentView(R.layout.activity_main);
    }
}

MyService.java

public class MyService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(()->{
            for (int i=0;i<100;i++){
                Log.d("Thomas", i+"");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
}

無法靜態註冊的廣播

以下的廣播, 無法使用靜態註冊, 只能使用動態註冊. 其原因是效能因素及安全性考量

android.intent.action.SCREEN_ON
android.intent.action.SCREEN_OFF
android.intent.action.BATTERY_CHANGED
android.intent.action.CONFIGURATION_CHANGED
android.intent.action.TIME_TICK

以上需要使用動態方式啟動, 但如果是在Activity中啟動, 待Activity一結束, 監聽功能也就跟著消失, 所以解決方式通常是在Activity中啟動Service, 再於Service中啟動Receiver

TimeTickService.java

public class TimeTickService extends Service {
    TimeTickReceiver receiver;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public void onCreate() {
        if(receiver==null)receiver=new TimeTickReceiver();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        registerReceiver(receiver, new IntentFilter(Intent.ACTION_TIME_TICK));
        return Service.START_STICKY;
    }
    @Override
    public void onDestroy() {
        if(receiver!=null) {
            unregisterReceiver(receiver);
            receiver=null;
        }
    }
    public class TimeTickReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            long t= System.currentTimeMillis();
            Toast.makeText(context, t+"ms", Toast.LENGTH_LONG).show();
            Log.d("Thomas", t+"ms");
        }
    }
}

MainActivity.java啟動Service的方式如下

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        Intent intent=new Intent(this,TimeTickService.class);
        startService(intent);
        finish();
    }
}

完整程式碼下載 Ch19_TimeTick.rar

電話狀態廣播

系統電話狀態發生變化時, 會發出ACTION_PHONE_STATE_CHANGED的廣播. 此廣播可於設定檔進行靜態註冊. 而靜態註冊的特性為就算關掉Activity, 他還是在背景持續監聽, 而且就算是重新開機後, 還是一樣的會重啟監聽, 所以這類的程式, 就不需要使用Service來多此一舉了. 

此廣播可由其intent.getStringExtra(TelephonyManager.EXTRA_STATE)取得是何種狀態的變化

TelephonyManager.EXTRA_STATE_RINGING : 接到來電

此時又可由 intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) 取得來電的電話號碼

TelephonyManager.EXTRA_STATE_OFFHOOK : 掛斷

TelephonyManager.EXTRA_STATE_IDLE : 切換到閒置狀態

讀取電話狀態需於設定檔取得 android.permission.READ_PHONE_STATE 的權限, 但讀取電話狀態屬危險授權, 所以需於MainActivity進行權限要求, 如下

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_PHONE_STATE_PERMISSION=100;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        processPermission();
        finish();
    }
    public void processPermission(){
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){
            int hasPermission=checkSelfPermission(Manifest.permission.READ_PHONE_STATE);
            if(hasPermission!= PackageManager.PERMISSION_GRANTED){
                requestPermissions(
                        new String[]{Manifest.permission.READ_PHONE_STATE},
                        REQUEST_PHONE_STATE_PERMISSION);
            }
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if(requestCode==REQUEST_PHONE_STATE_PERMISSION){
            if (grantResults[0]==PackageManager.PERMISSION_GRANTED){
                Toast.makeText(this, "Got permission", Toast.LENGTH_LONG).show();
            }
            else{
                Toast.makeText(this, "No permission", Toast.LENGTH_LONG).show();
            }
        }
        else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }

    }
}

BroadcastReceiver的寫法, 就是把上述的三種狀態寫出來即可, 如下程式碼

public class PhoneReceiver extends BroadcastReceiver {
    String message;
    @Override
    public void onReceive(Context context, Intent intent) {
        String state=intent.getStringExtra(TelephonyManager.EXTRA_STATE);
        if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)){
            message="Ring : "+intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
        }
        else if(state.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)){
            message="OffHook";
        }
        else if(state.equals(TelephonyManager.EXTRA_STATE_IDLE)){
            message="Idle";
        }
        Toast.makeText(context,message, Toast.LENGTH_LONG).show();
    }
}

最後, 在設定檔靜態註冊一下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.ddns.mahaljsp.ch19_telephone">
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".PhoneReceiver">
            <intent-filter>
                <action android:name="android.intent.action.PHONE_STATE" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

完整程式碼下載 : Ch19_Telephone.rar

小米的自啟動管理

小米手機的安全中心/授權管理/自啟動管理 是作什麼用的呢?

這個功能其實很陽春, 他只是允許程式的BroadcastReceiver能接收到靜態註冊的廣播訊息而以. 比如Line這種程式都會伴隨著一支Receiver來接收BOOT_COMPLETED, 然後啟動Line的後台Service. 如果沒有允許Line的自啟動功能, 則會因為重新開機後無法收到BOOT_COMPLETED而無法自動啟動Line Service, 而造成別人Line你的時候你會收不到訊息

至於上述的Ch19_TimeTick程式, 因為沒有使用BroadcastReceiver來接收BOOT_COMPLETED, 也沒有用BroadcastReceiver來啟動Service, 所以就算把Ch19_TimeTick設為自啟動, Service也是不會啟動的.

再來說明一下Ch19_Telephone, 這支程式是靜態註冊. 一般手機都可以在重新開機後自動運作的, 但小米屏蔽了靜態註冊的廣播, 所以也是需要在安全中心打開自啟管理才可以在重新開機後自動運作. 

Wifi廣播

Wifi廣播可使用靜態註冊, 需先取得如下二個權限

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Wifi裝置開啟及關閉廣播

Java : WIFI_STATE_CHANGED_ACTION
AndroidManifest.xml : android.net.wifi.WIFI_STATE_CHANGED

連線變更廣播

Java : NETWORK_STATE_CHANGED_ACTION
AndroidManifest.xml : android.net.wifi.STATE_CHANGE

Receiver程式如下

public class WifiReceiver extends BroadcastReceiver {
    private boolean isShowSSID=false;
    String ssid="";
    @Override
    public void onReceive(Context context, Intent intent) {
        String action =intent.getAction();
        if(action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)){
            int state=intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0);
            switch(state){
                case WifiManager.WIFI_STATE_ENABLED:
                    Toast.makeText(context, "開啟Wifi", Toast.LENGTH_LONG).show();
                    break;
                case WifiManager.WIFI_STATE_DISABLED:
                    Toast.makeText(context, "關閉Wifi", Toast.LENGTH_LONG).show();
                    break;
            }
        }
        else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)){
            NetworkInfo networkInfo=intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
            if (networkInfo.isConnected()){
                WifiManager manager=(WifiManager)context.getSystemService(Context.WIFI_SERVICE);
                WifiInfo wifiInfo=manager.getConnectionInfo();
                if(!ssid.equals(wifiInfo.getSSID())){
                    ssid=wifiInfo.getSSID();
                    Toast.makeText(context, wifiInfo.getSSID(), Toast.LENGTH_LONG).show();
                }
            }
        }
    }
}

AndroidManifest.xml 程式碼如下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.ddns.mahaljsp.ch19_wifi">
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".WifiReceiver">
            <intent-filter>
                <action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
                <action android:name="android.net.wifi.STATE_CHANGE" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

完整程式碼下載 : Ch19_Wifi.rar

發佈留言

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