在本系列前几篇文章中,我们介绍了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。