Android Tutorial 第五堂(1)廣播接收元件 - BroadcastReceiver 與 AlarmManager by Michael | CodeData
top

Android Tutorial 第五堂(1)廣播接收元件 - BroadcastReceiver 與 AlarmManager

分享:

專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3)

Android Tutorial 第四堂(3)讀取裝置目前的位置 – Google Services Location << 前情

Android系統有一種特別的「廣播事件」,它可以在系統或其它應用程式發生一些事件的時候,通知需要的應用程式執行一些指定的工作。例如裝置在接到來電的時候,系統會發出一個來電的廣播事件,如果應用程式需要在裝置來電的時候執行一些工作,可以設計一個接收來電廣播事件的「廣播接收元件」。

廣播接收元件是一個繼承自「android.content.BroadcastReceiver」的子類別,在這個類別中實作接收到廣播事件後需要執行的工作。Android系統在很多不同的情況都會發出廣播事件,你可以依照應用程式的需求,為廣播接收元件設定它要接收與處理哪一種廣播事件。

如果需要的話,應用程式也可以發出自己定義的廣播事件,這樣的作法只有在需要與別的應用程式互動的時候,才會執行這樣的工作。這一章會說明發出廣播與設計廣播接收元件的作法,為記事資料加入提醒的功能。選擇一個記事資料以後可以選擇設定提醒的功能:

AndroidTutorial5_05_01_01

使用者可以依照自己的需求,選擇提醒的日期與時間:

AndroidTutorial5_05_01_02AndroidTutorial5_05_01_03

使用者設定的日期與時間到了以後,會使用記事的標題顯示訊息框:

AndroidTutorial5_05_01_04

後續的內容會把訊息框改為系統的通知(Notification)。

15-1 發送與接收廣播事件

在一些特別的情況下,應用程式需要發送自己定義廣播事件,裝置中的其它應用程式可以接收與處理這個廣播事件。系統或應用程式自己定義的廣播事件,都是使用Action名稱來識別它是哪一種廣播事件,所以要為自己定義的廣播事件取一個Action名稱,再使用這個名稱發送廣播事件。呼叫Activity元件提供的「sendBroadcast」方法可以發送廣播事件,它需要一個設定好Action名稱的Intent物件,你也可以在Intent物件中設定一些資料,這些資料可以傳送給處理的廣播接收元件使用。下面這個程式片段示範發送自己定義的廣播事件作法:

// 發送廣播事件用的Action名稱
public static final String BROADCAST_ACTION =
        "net.macdidi.broadcast01.action.MYBROADCAST01";
...
// 建立準備發送廣播事件的Intent物件
Intent intent = new Intent(BROADCAST_ACTION);
// 如果需要的話,也可以設定資料到Intent物件
intent.putExtra("name", nameValue);
intent.putExtra("age", ageValue);
// 發送廣播事件
sendBroadcast(intent);

廣播接收元件是一個繼承自「android.content.BroadcastReceiver」的子類別,它的任務是在接收到廣播事件後執行一些工作,這個元件只需要實作「onReceive」方法,在方法中實作接收到指定廣播事件以後需要執行的工作。下面的程式片段示範基本的廣播接收元件作法:

// 繼承自BroadcastReceiver的廣播接收元件
public class MyBroadcastReceiver extends BroadcastReceiver {
    // 接收廣播後執行這個方法
    // 第一個參數Context物件,用來顯示訊息框、啟動服務
    // 第二個參數是發出廣播事件的Intent物件,可以包含資料
    @Override
    public void onReceive(Context context, Intent intent) {
        // 讀取包含在Intent物件中的資料
        String name = intent.getStringExtra("name");
        int age = intent.getIntExtra("age", -1);
        ...
        // 因為這不是Activity元件,需要使用Context物件的時候,
        // 不可以使用「this」,要使用參數提供的Context物件
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    }
}

廣播接收元件在設計好以後,一定要在應用程式設定檔中使用「receiver」標籤加入設定,在標籤中設定Action名稱決定它接收哪一種廣播事件:

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application … >
        <!-- 使用receiver標籤,名稱設定廣播接收元件類別名稱 -->
        <receiver android:name="MyBroadcastReceiver">
            <intent-filter>
                <!-- 使用Action名稱設定接收的廣播事件 -->
                <action android:name=
                    "net.macdidi.broadcast01.action.MYBROADCAST01" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

完成廣播接收元件和需要的設定,應用程式安裝到裝置以後,廣播接收元件就會在等待指定廣播事件的狀態,系統在偵測到指定的廣播事件,就會呼叫這個廣播接收元件的onReceive方法。

在應用程式設定檔中執行廣播接收元件的設定,會讓這個廣播接收元件一直在等待接收的狀態。如果應用程式只需要在運作的時候接收廣播事件,就不要在應用程式設定檔中執行設定,應該在元件中使用程式碼執行註冊與移除廣播接收元件的工作。以Activity元件來說,在onResume方法中呼叫「registerReceiver」執行註冊的工作。在onPause方法中呼叫「unregisterReceiver」執行移除的工作。下面這個程式片段示範使用程式碼註冊與移除廣播接收元件的作法:

public static final String BROADCAST_ACTION =
        "net.macdidi.broadcast01.action.MYBROADCAST01";

// 建立廣播接收元件物件
MyBroadcastReceiver receiver = new MyBroadcastReceiver();
...
@Override
protected void onResume() {
    super.onResume();
    // 準備註冊與移除廣播接收元件的IntentFilter物件
    IntentFilter filter = new IntentFilter(Intent.ACTION_TIME_TICK);
    // 註冊廣播接收元件
    registerReceiver(receiver, filter);
}

@Override
protected void onPause() {
    // 移除廣播接收元件
    unregisterReceiver(receiver);
    super.onPause();
}

15-2 系統廣播事件

廣播接收元件主要的應用是接收特定的系統廣播事件,可以在系統發出廣播的時候執行一些需要的工作。Android系統規劃很多需要的系統廣播事件,也都為它們取好Action名稱,這些名稱都分類宣告在Android API。這是一些宣告在不同類別或套件下的廣播事件:

  • android.content.Intent – 主要的系統廣播事件都宣告在這個類別,例如裝置開機完成的Action名稱變數是「ACTIONBOOTCOMPLETED」,實際的名稱是「android.intent.action.BOOT_COMPLETED」。
  • android.bluetooth – 與藍牙設備相關的廣播事件。例如這個套件下的「BludtoothAdapter」類別,宣告接收藍牙設備狀態改變的廣播事件變數「ACTIONSTATECHANGED」。
  • android.hardware.Camera – 與相機設備相關的廣播事件。例如使用相機錄製影片與拍攝照片的廣播事件名稱,變數名稱為「ACTIONNEWVIDEO」與「ACTIONNEWPICTURE」。
  • android.net.wifi.WifiManager – 與Wifi網路設備相關的廣播事件。例如「NETWORKSTATECHANGED_ACTION」是網路狀態改變廣播事件的變數名稱。
  • android.media.AudioManager – 與裝置音效相關的廣播事件。例如裝置音效模式改變的變數名稱「RINGERMODECHANGED_ACTION」。
    主要的系統廣播事件宣告在Intent類別中,變數的名稱都是以「ACTION」開始,這些是比較常見的廣播事件:
  • ACTIONBOOTCOMPLETED – 系統裝置完成開機的工作後發送的廣播事件,如果要接收這個廣播事件,要加入「RECEIVEBOOTCOMPLETED」的授權設定到應用程式設定檔中。
  • ACTIONTIMETICK – 系統固定每一分鐘發送一次這個廣播事件。
  • ACTIONDATECHANGED、ACTIONTIMECHANGED – 改變系統的日期與時間的時候發送的廣播事件。

如果應用程式需要在系統開機完成後執行一些特定的工作,使用在Intent類別宣告的「ACTIONBOOTCOMPLETED」廣播事件名稱變數,它的Action名稱是「android.intent.action.BOOT_COMPLETED」。像這類在固定情況下送出的系統廣播事件,應該在應用程式設定中執行註冊的工作。下面這個程式片段示範接收系統開機完成事件的廣播元件作法:

//繼承自BroadcastReceiver的廣播接收元件
public class BootCompletedReceiver extends BroadcastReceiver {
    // 接收廣播後執行這個方法
    // 第一個參數Context物件,用來顯示訊息框、啟動服務
    // 第二個參數是發出廣播事件的Intent物件,可以包含資料    
    @Override
    public void onReceive(Context context, Intent intent) {
        // 執行廣播元件的工作
    }
}

在應用程式設定檔使用「receiver」標籤加入設定,在標籤中設定Action名稱的時候,要使用廣播事件實際的Action名稱,它們的名稱都可以在Android API文件中查詢。下面這個片段示範在應用程式設定檔中執行設定的作法:

<?xml version="1.0" encoding="utf-8"?>
<manifest … >
    <application … >
        <!-- 使用receiver標籤,名稱設定廣播接收元件類別名稱 -->
        <receiver android:name="BootCompletedReceiver">
            <intent-filter>
                <!-- 設定系統啟動完成的Action名稱 -->
                <action android:name=
                    "android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

15-3 修改記事類別與資料庫

瞭解廣播事件與廣播接收元件基本的概念以後,就可以為記事應用程式加入提醒的功能。目前因為記事沒有儲存提醒日期時間的資料,所以需要加入相關的修改,包含記事類別與資料庫。

開啟「Item.java」,依照下列的程式片段加入提醒的日期時間資料:

// 提醒日期時間
private long alarmDatetime;
...
public long getAlarmDatetime() {
    return alarmDatetime;
}

public void setAlarmDatetime(long alarmDatetime) {
    this.alarmDatetime = alarmDatetime;
}

為了讓提醒的日期時間資料也可以儲存在資料庫,開啟「ItemDAO.java」,參考下列的程式片段,加入新的欄位定義變數與修改建立表格的敘述:

// 提醒日期時間
public static final String ALARMDATETIME_COLUMN = "alarmdatetime";

// 使用上面宣告的變數建立表格的SQL指令
public static final String CREATE_TABLE =
        "CREATE TABLE " + TABLE_NAME + " (" +
                KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                DATETIME_COLUMN + " INTEGER NOT NULL, " +
                COLOR_COLUMN + " INTEGER NOT NULL, " +
                TITLE_COLUMN + " TEXT NOT NULL, " +
                CONTENT_COLUMN + " TEXT NOT NULL, " +
                FILENAME_COLUMN + " TEXT, " +
                LATITUDE_COLUMN + " REAL, " +
                LONGITUDE_COLUMN + " REAL, " +
                LASTMODIFY_COLUMN + " INTEGER, " +
                ALARMDATETIME_COLUMN + " INTEGER)";

同樣在「ItemDAO.java」,修改負責新增記事資料的「insert」方法:

// 新增參數指定的物件
public Item insert(Item item) {
    ContentValues cv = new ContentValues();

    cv.put(DATETIME_COLUMN, item.getDatetime());
    cv.put(COLOR_COLUMN, item.getColor().parseColor());
    cv.put(TITLE_COLUMN, item.getTitle());
    cv.put(CONTENT_COLUMN, item.getContent());
    cv.put(FILENAME_COLUMN, item.getFileName());
    cv.put(LATITUDE_COLUMN, item.getLatitude());
    cv.put(LONGITUDE_COLUMN, item.getLongitude());
    cv.put(LASTMODIFY_COLUMN, item.getLastModify());

    // 提醒日期時間
    cv.put(ALARMDATETIME_COLUMN, item.getAlarmDatetime());

    long id = db.insert(TABLE_NAME, null, cv);

    item.setId(id);
    return item;
}

同樣在「ItemDAO.java」,修改負責修改記事資料的「update」方法:

// 修改參數指定的物件
public boolean update(Item item) {
    ContentValues cv = new ContentValues();

    cv.put(DATETIME_COLUMN, item.getDatetime());
    cv.put(COLOR_COLUMN, item.getColor().parseColor());
    cv.put(TITLE_COLUMN, item.getTitle());
    cv.put(CONTENT_COLUMN, item.getContent());
    cv.put(FILENAME_COLUMN, item.getFileName());
    cv.put(LATITUDE_COLUMN, item.getLatitude());
    cv.put(LONGITUDE_COLUMN, item.getLongitude());
    cv.put(LASTMODIFY_COLUMN, item.getLastModify());

    // 提醒日期時間
    cv.put(ALARMDATETIME_COLUMN, item.getAlarmDatetime());

    String where = KEY_ID + "=" + item.getId();
    return db.update(TABLE_NAME, cv, where, null) > 0;
}

同樣在「ItemDAO.java」,修改負責讀取記事資料的「getRecord」方法:

// 把Cursor目前的資料包裝為物件
public Item getRecord(Cursor cursor) {
    Item result = new Item();

    result.setId(cursor.getLong(0));
    result.setDatetime(cursor.getLong(1));
    result.setColor(ItemActivity.getColors(cursor.getInt(2)));
    result.setTitle(cursor.getString(3));
    result.setContent(cursor.getString(4));
    result.setFileName(cursor.getString(5));
    result.setLatitude(cursor.getDouble(6));
    result.setLongitude(cursor.getDouble(7));
    result.setLastModify(cursor.getLong(8));

    // 提醒日期時間
    result.setAlarmDatetime(cursor.getLong(9));

    return result;
}

因為已經修改資料表的架構,所以要修改資料庫的版本編號,開啟「MyDBHelper.java」,參考下面的程式片段,把資料庫的版本編號修改為2:

// 資料庫版本,資料結構改變的時候要更改這個數字,通常是加一
public static final int VERSION = 2;

15-4 記事鬧鈴提醒功能

完成基本類別與資料庫的修改後,接下來就可以為應用程式加入提醒的操作功能。開啟「ItemActivity.java」,加入下列設定提醒日期時間的方法宣告:

// 設定提醒日期時間
private void processSetAlarm() {
    Calendar calendar = Calendar.getInstance();

    if (item.getAlarmDatetime() != 0) {
        // 設定為已經儲存的提醒日期時間
        calendar.setTimeInMillis(item.getAlarmDatetime());
    }

    // 讀取年、月、日、時、分
    int year = calendar.get(Calendar.YEAR);
    int month = calendar.get(Calendar.MONTH);
    int day = calendar.get(Calendar.DAY_OF_MONTH);
    int hour = calendar.get(Calendar.HOUR_OF_DAY);
    int minute = calendar.get(Calendar.MINUTE);

    // 儲存設定的提醒日期時間
    final Calendar alarm = Calendar.getInstance();

    // 設定提醒時間
    TimePickerDialog.OnTimeSetListener timeSetListener =
            new TimePickerDialog.OnTimeSetListener() {
        @Override
        public void onTimeSet(TimePicker view,
                              int hourOfDay, int minute) {
            alarm.set(Calendar.HOUR_OF_DAY, hourOfDay);
            alarm.set(Calendar.MINUTE, minute);

            item.setAlarmDatetime(alarm.getTimeInMillis());
        }
    };

    // 選擇時間對話框
    final TimePickerDialog tpd = new TimePickerDialog(
            this, timeSetListener, hour, minute, true);

    // 設定提醒日期
    DatePickerDialog.OnDateSetListener dateSetListener =
            new DatePickerDialog.OnDateSetListener() {
        @Override
        public void onDateSet(DatePicker view,
                              int year,
                              int monthOfYear,
                              int dayOfMonth) {
            alarm.set(Calendar.YEAR, year);
            alarm.set(Calendar.MONTH, monthOfYear);
            alarm.set(Calendar.DAY_OF_MONTH, dayOfMonth);

            // 繼續選擇提醒時間
            tpd.show();
        }
    };

    // 建立與顯示選擇日期對話框
    final DatePickerDialog dpd = new DatePickerDialog(
            this, dateSetListener, year, month, day);
    dpd.show();
}

同樣在「ItemActivity.java」,在「clickFunction」方法加入啟動設定日期時間功能的敘述:

public void clickFunction(View view) {
    int id = view.getId();

    switch (id) {
        ...
        case R.id.set_alarm:
            // 設定提醒日期時間
            processSetAlarm();
            break;
        ...
    }

}

開啟「MainActivity.java」,找到「onActivityResult」方法,加入使用「AlarmManager」執行提醒功能的程式碼:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == Activity.RESULT_OK) {
        Item item = (Item) data.getExtras().getSerializable(
                "net.macdidi.myandroidtutorial.Item");

        // 是否修改提醒設定
        boolean updateAlarm = false;

        if (requestCode == 0) {
            item = itemDAO.insert(item);

            items.add(item);
            itemAdapter.notifyDataSetChanged();
            // 2015-06-25,修正新增記事提醒設定失效的錯誤
            updateAlarm = true;
        }
        else if (requestCode == 1) {
            int position = data.getIntExtra("position", -1);

            if (position != -1) {
                // 讀取原來的提醒設定
                Item ori = itemDAO.get(item.getId());
                // 判斷是否需要設定提醒
                updateAlarm = (item.getAlarmDatetime() != ori.getAlarmDatetime());

                itemDAO.update(item);
                items.set(position, item);
                itemAdapter.notifyDataSetChanged();
            }
        }

        // 設定提醒
        if (item.getAlarmDatetime() != 0 && updateAlarm) {
            Intent intent = new Intent(this, AlarmReceiver.class);
            intent.putExtra("title", item.getTitle());

            PendingIntent pi = PendingIntent.getBroadcast(
                    this, (int)item.getId(),
                    intent, PendingIntent.FLAG_ONE_SHOT);

            AlarmManager am = (AlarmManager)
                    getSystemService(Context.ALARM_SERVICE);
            am.set(AlarmManager.RTC_WAKEUP, item.getAlarmDatetime(), pi);
        }
    }
}

為了接收系統的提醒廣播事件,依照下列的步驟,為應用程式新增一個廣播接收元件:

  1. 在「app」目錄上按滑鼠右鍵 -> 選擇「Other」 -> 選擇「Broadcast Receiver」:
  2. 在「Class Name」欄位輸入「AlarmReceiver」。
  3. 選擇「Finish」。

參考下列的程式片段,在建立好的廣播接收元件類別,修改「onReceive」方法的程式碼:

@Override
public void onReceive(Context context, Intent intent) {
    // 讀取記事標題
    String title = intent.getStringExtra("title");
    // 顯示訊息框
    Toast.makeText(context, title, Toast.LENGTH_LONG).show();
}

15-5 開機完成廣播事件

依照上面的說明已經完成記事提醒的功能,不過使用「AlarmManager」執行提醒的工作,在Android系統重新開機以後就會失效,所以需要設計接收開機完成的廣播接收元件,重新執行設定提醒的工作。依照下列的步驟,為應用程式新增一個廣播接收元件:

  1. 在「app」目錄上按滑鼠右鍵 -> 選擇「Other」 -> 選擇「Broadcast Receiver」:
  2. 在「Class Name」欄位輸入「InitAlarmReceiver」。
  3. 選擇「Finish」。

參考下列的程式片段,在建立好的廣播接收元件類別,修改「onReceive」方法的程式碼:

@Override
public void onReceive(Context context, Intent intent) {
    // 建立資料庫物件
    ItemDAO itemDAO = new ItemDAO(context.getApplicationContext());
    // 讀取資料庫所有記事資料
    List<Item> items = itemDAO.getAll();

    // 讀取目前時間
    long current = Calendar.getInstance().getTimeInMillis();

    AlarmManager am = (AlarmManager)
            context.getSystemService(Context.ALARM_SERVICE);

    for (Item item : items) {
        long alarm = item.getAlarmDatetime();

        // 如果沒有設定提醒或是提醒已經過期
        if (alarm == 0 || alarm <= current) {
            continue;
        }

        // 設定提醒
        Intent alarmIntent = new Intent(context, AlarmReceiver.class);
        alarmIntent.putExtra("title", item.getTitle());

        PendingIntent pi = PendingIntent.getBroadcast(
                context, (int)item.getId(),
                alarmIntent, PendingIntent.FLAG_ONE_SHOT);
        am.set(AlarmManager.RTC_WAKEUP, item.getAlarmDatetime(), pi);
    }
}

開啟應用程式的設定檔「AndroidMainfest.xml」,在「manifest」標籤下加入接收開機完成的廣播事件授權設定:

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

同樣在「AndroidMainfest.xml」,找到Android Studio自動加入的廣播接收元件設定,參考下面的程式片段,加入需要的設定:

<receiver
    android:name=".InitAlarmReceiver"
    android:enabled="true"
    android:exported="true" >
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

完成這一章所有的工作了,這個部份的功能可以在模擬或實體裝置測試。開啟記事資料,為它設定一分鐘後的提醒,看看是不是可以正確的顯示訊息框。

課程相關的檔案都可以GitHub瀏覽與下載。

http://github.com/macdidi5/AndroidTutorial

後續 >> Android Tutorial 第五堂(2)系統通知服務 – Notification

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

熱門論壇文章

熱門技術文章