在本系列前幾篇文章中,我們介紹了Android Wear計時器app,對設計思路和app的結構進行了分析。本文將講解如何定時喚醒程式提醒使用者。
對於為什麼不用後臺服務的方式一直執行,我們已經進行了解釋——這種方式非常耗電。因此,我們必須要有一個定時喚醒機制。我們可以使用AlarmManager來實現這個機制,定時執行一個Intent,然後通知BroadcastReceiver。之所以選擇BroadcastReceiver而不用IntentService,是因為我們要執行的任務是輕量級的而且生命週期非常短暫。使用BroadcastReceiver可以避免每次執行任務的時候都經歷Service的整個生命週期。因此,對於我們這種輕量級的任務來說非常合適——我們執行的任務都在毫秒級。
BroadcastReceiver的核心在於onReceiver方法,我們需要在這裡安排各種事件響應。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
; html-script: false ] public class MatchTimerReceiver extends BroadcastReceiver { public static final int MINUTE_MILLIS = 60000; private static final long DURATION = 45 * MINUTE_MILLIS; private static final Intent UPDATE_INTENT = new Intent(ACTION_UPDATE); private static final Intent ELAPSED_ALARM = new Intent(ACTION_ELAPSED_ALARM); private static final Intent FULL_TIME_ALARM = new Intent(ACTION_FULL_TIME_ALARM); private static final int REQUEST_UPDATE = 1; private static final int REQUEST_ELAPSED = 2; private static final int REQUEST_FULL_TIME = 3; public static void setUpdate(Context context) { context.sendBroadcast(UPDATE_INTENT); } . . . private void reset(MatchTimer timer) { timer.reset(); } private void resume(Context context, MatchTimer timer) { timer.resume(); long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION; if (playedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd); } } private void pause(Context context, MatchTimer timer) { timer.pause(); cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM); long elapsedEnd = timer.getStartTime() + DURATION; if (!isAlarmSet(context, REQUEST_ELAPSED, ELAPSED_ALARM) && elapsedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd); } } private void stop(Context context, MatchTimer timer) { timer.stop(); cancelAlarm(context, REQUEST_UPDATE, UPDATE_INTENT); cancelAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM); cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM); } private void start(Context context, MatchTimer timer) { timer.start(); long elapsedEnd = timer.getStartTime() + DURATION; setRepeatingAlarm(context, REQUEST_UPDATE, UPDATE_INTENT); if (timer.getTotalStoppages() > 0 && !timer.isPaused()) { long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION; if (playedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd); } if (elapsedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd); } } else { if (elapsedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, elapsedEnd); } } } . . . } |
程式碼還是非常直觀易於理解的。首先例項化一個MatchTimer物件(從SharedPreference中讀取資料),然後分別傳給對應的事件處理Handler。之後等待動作發生,最後更新Notification。
這裡會處理8個事件動作,其中5個負責控制計時器的狀態(START、STOP、PAUSE、RESUME、RESET);一個負責更新Notification,剩下兩個負責到45分鐘喚醒後震動提示。
我們先從這幾個控制狀態開始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
; html-script: false ] public class MatchTimerReceiver extends BroadcastReceiver { public static final int MINUTE_MILLIS = 60000; private static final long DURATION = 45 * MINUTE_MILLIS; private static final Intent UPDATE_INTENT = new Intent(ACTION_UPDATE); private static final Intent ELAPSED_ALARM = new Intent(ACTION_ELAPSED_ALARM); private static final Intent FULL_TIME_ALARM = new Intent(ACTION_FULL_TIME_ALARM); private static final int REQUEST_UPDATE = 1; private static final int REQUEST_ELAPSED = 2; private static final int REQUEST_FULL_TIME = 3; public static void setUpdate(Context context) { context.sendBroadcast(UPDATE_INTENT); } . . . private void reset(MatchTimer timer) { timer.reset(); } private void resume(Context context, MatchTimer timer) { timer.resume(); long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION; if (playedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd); } } private void pause(Context context, MatchTimer timer) { timer.pause(); cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM); long elapsedEnd = timer.getStartTime() + DURATION; if (!isAlarmSet(context, REQUEST_ELAPSED, ELAPSED_ALARM) && elapsedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd); } } private void stop(Context context, MatchTimer timer) { timer.stop(); cancelAlarm(context, REQUEST_UPDATE, UPDATE_INTENT); cancelAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM); cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM); } private void start(Context context, MatchTimer timer) { timer.start(); long elapsedEnd = timer.getStartTime() + DURATION; setRepeatingAlarm(context, REQUEST_UPDATE, UPDATE_INTENT); if (timer.getTotalStoppages() > 0 && !timer.isPaused()) { long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION; if (playedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd); } if (elapsedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd); } } else { if (elapsedEnd > System.currentTimeMillis()) { setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, elapsedEnd); } } } . . . } |
這些方法主要有兩個功能:首先設定MatchTimer的狀態,然後設定時間提醒的鬧鈴,改變引數就可以播放鬧鈴。這個功能還可以封裝成一個工具方法,叫setUpdate()。這樣外部也可以觸發計時器的更新。
我們使用標準AlarmManager的方法來設定鬧鈴:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
; html-script: false ] public class MatchTimerReceiver extends BroadcastReceiver { . . . public static final int MINUTE_MILLIS = 60000; . . . private void setRepeatingAlarm(Context context, int requestCode, Intent intent) { AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MINUTE_MILLIS, pendingIntent); } private boolean isAlarmSet(Context context, int requestCode, Intent intent) { return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_NO_CREATE) != null; } private void setAlarm(Context context, int requestCode, Intent intent, long time) { AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); } private void cancelAlarm(Context context, int requestCode, Intent intent) { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_NO_CREATE); if (pendingIntent != null) { AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmManager.cancel(pendingIntent); pendingIntent.cancel(); } } . . . } |
這裡值得討論的是setRepeatingAlarm()這個方法。因為在Wear在實現方式上有點不一樣。我們會在Start事件中每秒鐘觸發一次鬧鈴更新Notification動作,所以這裡需要記錄具體已經過去了多少分鐘。正常來說我們會每隔60秒觸發一次這個動作,但是在Wear上不能這麼做。原因是——當裝置在喚醒著的時候可以這樣做,但是如果裝置進入睡眠狀態就需要重新計算下一分鐘的邊界值。這就需要非同步更新部件,然後裝置只需要每分鐘喚醒一次。一分鐘結束後在計時器需要更新狀態的時候觸發操作。
對於我們的計時器應用來說,顯示的分鐘數會比實際時間少1分鐘。但是顯示分鐘並不要求非常實時(但顯示秒數時需要非常精確),所以我們可以這樣操作:
完整的alarm Handler是這樣使用振動服務的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
; html-script: false ] public class MatchTimerReceiver extends BroadcastReceiver { . . . private static final long[] ELAPSED_PATTERN = {0, 500, 250, 500, 250, 500}; private static final long[] FULL_TIME_PATTERN = {0, 1000, 500, 1000, 500, 1000}; private void elapsedAlarm(Context context) { Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(ELAPSED_PATTERN, -1); } private void fullTimeAlarm(Context context) { Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(FULL_TIME_PATTERN, -1); } . . . } |
最後,我們通過這個方法來構造Notification然後呈現給使用者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
; html-script: false ] public class MatchTimerReceiver extends BroadcastReceiver { public static final int NOTIFICATION_ID = 1; . . . private void updateNotification(Context context, MatchTimer timer) { NotificationBuilder builder = new NotificationBuilder(context, timer); Notification notification = builder.buildNotification(); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(NOTIFICATION_ID, notification); } } |
Notification是Wear計時器的一個重要的部分,這裡還需要一個自定義類來構造這些Notification通知。下一篇文章我們會講如何在計時器app中使用Notification。
Match Timer可以在Google Play上下載:Match Timer。