專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3)
Android 6 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。這是一些宣告在不同類別或套件下的廣播事件:
- 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, " +
RECFILENAME_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(RECFILENAME_COLUMN, item.getRecFileName());
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(RECFILENAME_COLUMN, item.getRecFileName());
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.setRecFileName(cursor.getString(6));
result.setLatitude(cursor.getDouble(7));
result.setLongitude(cursor.getDouble(8));
result.setLastModify(cursor.getLong(9));
// 提醒日期時間
result.setAlarmDatetime(cursor.getLong(10));
return result;
}
因為已經修改資料表的架構,所以要修改資料庫的版本編號,開啟「MyDBHelper.java」,參考下面的程式片段,把資料庫的版本編號修改為2:
// 資料庫版本,資料結構改變的時候要更改這個數字,通常是加一
public static final int VERSION = 2;
15-4 記事鬧鈴提醒功能
完成基本類別與資料庫的修改後,接下來就可以為應用程式加入提醒的操作功能。為了接收系統的提醒廣播事件,依照下列的步驟,為應用程式新增一個廣播接收元件:
- 在「app」目錄上按滑鼠右鍵 -> 選擇「Other」 -> 選擇「Broadcast Receiver」:
- 在「Class Name」欄位輸入「AlarmReceiver」。
- 選擇「Finish」。
參考下列的程式片段,在建立好的廣播接收元件類別,修改「onReceive」方法的程式碼:
@Override
public void onReceive(Context context, Intent intent) {
// 讀取記事標題
String title = intent.getStringExtra("title");
// 顯示訊息框
Toast.makeText(context, title, Toast.LENGTH_LONG).show();
}
開啟「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();
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);
}
}
}
15-5 開機完成廣播事件
依照上面的說明已經完成記事提醒的功能,不過使用「AlarmManager」執行提醒的工作,在Android系統重新開機以後就會失效,所以需要設計接收開機完成的廣播接收元件,重新執行設定提醒的工作。依照下列的步驟,為應用程式新增一個廣播接收元件:
- 在「app」目錄上按滑鼠右鍵 -> 選擇「Other」 -> 選擇「Broadcast Receiver」:
- 在「Class Name」欄位輸入「InitAlarmReceiver」。
- 選擇「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/Android-6-Tutorial
後續 >> Android 6 Tutorial 第五堂(2)系統通知服務 – Notification
|