Android Tutorial 第五堂(1)廣播接收元件 - BroadcastReceiver 與 AlarmManager
專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3) Android Tutorial 第四堂(3)讀取裝置目前的位置 – Google Services Location << 前情 Android系統有一種特別的「廣播事件」,它可以在系統或其它應用程式發生一些事件的時候,通知需要的應用程式執行一些指定的工作。例如裝置在接到來電的時候,系統會發出一個來電的廣播事件,如果應用程式需要在裝置來電的時候執行一些工作,可以設計一個接收來電廣播事件的「廣播接收元件」。 廣播接收元件是一個繼承自「android.content.BroadcastReceiver」的子類別,在這個類別中實作接收到廣播事件後需要執行的工作。Android系統在很多不同的情況都會發出廣播事件,你可以依照應用程式的需求,為廣播接收元件設定它要接收與處理哪一種廣播事件。 如果需要的話,應用程式也可以發出自己定義的廣播事件,這樣的作法只有在需要與別的應用程式互動的時候,才會執行這樣的工作。這一章會說明發出廣播與設計廣播接收元件的作法,為記事資料加入提醒的功能。選擇一個記事資料以後可以選擇設定提醒的功能: 使用者可以依照自己的需求,選擇提醒的日期與時間: 使用者設定的日期與時間到了以後,會使用記事的標題顯示訊息框: 後續的內容會把訊息框改為系統的通知(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。這是一些宣告在不同類別或套件下的廣播事件:
如果應用程式需要在系統開機完成後執行一些特定的工作,使用在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); } } } 為了接收系統的提醒廣播事件,依照下列的步驟,為應用程式新增一個廣播接收元件:
參考下列的程式片段,在建立好的廣播接收元件類別,修改「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系統重新開機以後就會失效,所以需要設計接收開機完成的廣播接收元件,重新執行設定提醒的工作。依照下列的步驟,為應用程式新增一個廣播接收元件:
參考下列的程式片段,在建立好的廣播接收元件類別,修改「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瀏覽與下載。 |