Android系统闹钟定时功能框架,总体来说就是用数据库存储定时数据,有一个状态管理器来统一管理这些定时状态的触发和更新。在Andriod系统中实现定时功能,最终还是要用到系统提供的AlarmManager,只是当一个定时完成后怎么继续处理,或者中间怎么更新定时的时间或者状态,像闹钟这种应用程序,每天重复定时,或者一周选择其中的几天,闹钟响了延迟5分钟再次响铃,这时候就需要想一种好的办法来让管理这些数据和状态,下面就分析一下Android系统闹钟的实现。

1、基本结构


Alarm

代表一条定时数据

AlarmInstance

代表一个定时项目的实例,一个AlarmInstance对应到一个Alarm,相比Alarm多存储了一些状态信息

AlarmStateManager

状态管理器,对定时项目进行调度,添加、删除、更改状态,是一个BroadcastReciever,定时到点后发广播到这里进行下一步处理

AlarmService

响应结果,也就是定时到达后要做的事,响铃,停止响铃

ClockDataHelper

里面创建了三个表,ALARMS_TABLE,INSTANCE_TABLE,CITIES_TABLE,前两个分别对应到上面的Alarm和AlarmInstance。

  1. private static void createAlarmsTable(SQLiteDatabase db) {
  2. db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +
  3. ClockContract.AlarmsColumns._ID + " INTEGER PRIMARY KEY," +
  4. ClockContract.AlarmsColumns.HOUR + " INTEGER NOT NULL, " +
  5. ClockContract.AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +
  6. ClockContract.AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +
  7. ClockContract.AlarmsColumns.ENABLED + " INTEGER NOT NULL, " +
  8. ClockContract.AlarmsColumns.VIBRATE + " INTEGER NOT NULL, " +
  9. ClockContract.AlarmsColumns.LABEL + " TEXT NOT NULL, " +
  10. ClockContract.AlarmsColumns.RINGTONE + " TEXT, " +
  11. ClockContract.AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);");
  12. Log.i("Alarms Table created");
  13. }
  1. private static void createInstanceTable(SQLiteDatabase db) {
  2. db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +
  3. ClockContract.InstancesColumns._ID + " INTEGER PRIMARY KEY," +
  4. ClockContract.InstancesColumns.YEAR + " INTEGER NOT NULL, " +
  5. ClockContract.InstancesColumns.MONTH + " INTEGER NOT NULL, " +
  6. ClockContract.InstancesColumns.DAY + " INTEGER NOT NULL, " +
  7. ClockContract.InstancesColumns.HOUR + " INTEGER NOT NULL, " +
  8. ClockContract.InstancesColumns.MINUTES + " INTEGER NOT NULL, " +
  9. ClockContract.InstancesColumns.VIBRATE + " INTEGER NOT NULL, " +
  10. ClockContract.InstancesColumns.LABEL + " TEXT NOT NULL, " +
  11. ClockContract.InstancesColumns.RINGTONE + " TEXT, " +
  12. ClockContract.InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +
  13. ClockContract.InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +
  14. ALARMS_TABLE_NAME + "(" + ClockContract.AlarmsColumns._ID + ") " +
  15. "ON UPDATE CASCADE ON DELETE CASCADE" +
  16. ");");
  17. Log.i("Instance table created");
  18. }

这里说一下几个特殊的字段,对于Alarm表,DAYS_OF_WEEK表示一周内需要定时的天(闹钟有个功能是选择一周中的几天),这里是个int值,用位来表示设置的天数,源码中有个专门的类DaysOfWeek来存储和处理。

AlarmInstance表中有一个ALARM_ID,关联到一个Alarm,可以看到在AlarmInstance表里也有时间,为什么不和Alarm表合成一个表?应该是这样的,Alarm表示原始的定时项,是一个基础数据,而AlarmInstance则代表了一个使用中的定时项目,或者是一个已经激活的定时项目,它的时间是可以变化的,比如闹钟响了以后延时5分钟再响,就需要改变这里的时间,而基础数据不能变,还需要显示在那里。ALARM_STATE代表了当前定时项目的状态,具体调度都在AlarmStateManager中管理。

忘了在哪里看到的,“编程最重要的是设计数据结构,接下来是分解各种代码块”。数据结构是基础,就像建筑里的钢筋水泥砖瓦,有了基础的材料后,剩下的工作就是对这些材料处理,也就是设计具体的处理逻辑。

2、具体的类分析


Alarm

从上面也可以看出,Alarm类作为定时的基础数据结构,主要是封装了一些数据库操作,完成增删改查功能。额外有一个方法createInstanceAfter,根据自身来创建一个AlarmInstance实例。代码如下

  1. public AlarmInstance createInstanceAfter(Calendar time) {
  2. Calendar nextInstanceTime = Calendar.getInstance();
  3. nextInstanceTime.set(Calendar.YEAR, time.get(Calendar.YEAR));
  4. nextInstanceTime.set(Calendar.MONTH, time.get(Calendar.MONTH));
  5. nextInstanceTime.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH));
  6. nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
  7. nextInstanceTime.set(Calendar.MINUTE, minutes);
  8. nextInstanceTime.set(Calendar.SECOND, 0);
  9. nextInstanceTime.set(Calendar.MILLISECOND, 0);
  10. // If we are still behind the passed in time, then add a day
  11. if (nextInstanceTime.getTimeInMillis() <= time.getTimeInMillis()) {
  12. nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
  13. }
  14. // The day of the week might be invalid, so find next valid one
  15. int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime);
  16. if (addDays > 0) {
  17. nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
  18. }
  19. AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
  20. result.mVibrate = vibrate;
  21. result.mLabel = label;
  22. result.mRingtone = alert;
  23. return result;
  24. }

AlarmInstance

AlarmInstance与Alarm很相似,像Alarm中的增删改查操作在AlarmInstance中都有相似的方法。那有什么不同呢,就是上面说的AlarmInstance的时间是可以根据当前状态改变的,也就多了时间的set和get方法。

  1. public void setAlarmTime(Calendar calendar) {
  2. mYear = calendar.get(Calendar.YEAR);
  3. mMonth = calendar.get(Calendar.MONTH);
  4. mDay = calendar.get(Calendar.DAY_OF_MONTH);
  5. mHour = calendar.get(Calendar.HOUR_OF_DAY);
  6. mMinute = calendar.get(Calendar.MINUTE);
  7. }
  8. /**
  9. * Return the time when a alarm should fire.
  10. *
  11. * @return the time
  12. */
  13. public Calendar getAlarmTime() {
  14. Calendar calendar = Calendar.getInstance();
  15. calendar.set(Calendar.YEAR, mYear);
  16. calendar.set(Calendar.MONTH, mMonth);
  17. calendar.set(Calendar.DAY_OF_MONTH, mDay);
  18. calendar.set(Calendar.HOUR_OF_DAY, mHour);
  19. calendar.set(Calendar.MINUTE, mMinute);
  20. calendar.set(Calendar.SECOND, 0);
  21. calendar.set(Calendar.MILLISECOND, 0);
  22. return calendar;
  23. }

AlarmStateManager

闹钟定时的核心逻辑就在这里,AlarmStateManager就是管理所有定时项目状态的调度器。

可以看到上面大多是static类型的方法,用于设置各种状态值。

先看一下定时的几种状态:

SILENT_STATE,alarm被激活,但是不需要显示任何东西,下一个状态是LOW_NOTIFICATION_STATE;

LOW_NOTIFICATION_STATE,这个状态表示alarm离触发的时间不远了,时间差是AlarmInstance.LOW_NOTIFICATION_HOUR_OFFSET=-2,也就是2个小时。下一个状态会进入HIGH_NOTIFICATION_STATE,HIDE_NOTIFICATION_STATE,DISMISS_STATE;

HIDE_NOTIFICATION_STATE,这是一个暂时态,表示用户想隐藏掉通知,这个状态会一直持续到HIGH_NOTIFICATION_STATE;

HIGH_NOTIFICATION_STATE,这个状态和LOW_NOTIFICATION_STATE相似,但不允许用户隐藏通知,负责触发FIRED_STATE或者DISMISS_STATE;

SNOOZED_STATE,像HIGH_NOTIFICATION_STATE,但是会增加一点定时的时间来完成延迟功能;

FIRED_STATE,表示响铃状态,会启动AlarmService直到用户将其变为SNOOZED_STATE或者DISMISS_STATE,如果用户放任不管,会之后进入MISSED_STATE;

MISSED_STATE,这个状态在FIRED_STATE之后,会在通知栏给出一个提醒刚才响铃了;

DISMISS_STATE,这个状态表示定时结束了,会根据定时项目的设置判断是否需要重复,从而决定要删除这个项目还是继续设定一个新的定时。

上面的 setXXXState 方法就是对这些状态的处理,同时会规划一个定时转换到下一个状态。比如setSilentState:

  1. public static void setSilentState(Context context, AlarmInstance instance) {
  2. Log.v("Setting silent state to instance " + instance.mId);
  3. // Update alarm in db
  4. ContentResolver contentResolver = context.getContentResolver();
  5. instance.mAlarmState = AlarmInstance.SILENT_STATE;
  6. AlarmInstance.updateInstance(contentResolver, instance);
  7. // Setup instance notification and scheduling timers
  8. AlarmNotifications.clearNotification(context, instance);
  9. scheduleInstanceStateChange(context, instance.getLowNotificationTime(),
  10. instance, AlarmInstance.LOW_NOTIFICATION_STATE);
  11. }

更新AlarmInstance的信息,同时通过scheduleInstanceStateChange()规划下一个状态:

  1. private static void scheduleInstanceStateChange(Context context, Calendar time,
  2. AlarmInstance instance, int newState) {
  3. long timeInMillis = time.getTimeInMillis();
  4. Log.v("Scheduling state change " + newState + " to instance " + instance.mId +
  5. " at " + AlarmUtils.getFormattedTime(context, time) + " (" + timeInMillis + ")");
  6. Intent stateChangeIntent = createStateChangeIntent(context, ALARM_MANAGER_TAG, instance,
  7. newState);
  8. PendingIntent pendingIntent = PendingIntent.getBroadcast(context, instance.hashCode(),
  9. stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  10. AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
  11. if (Utils.isKitKatOrLater()) {
  12. am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
  13. } else {
  14. am.set(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
  15. }
  16. }

通过AlarmManager发起一个定时,定时的时间从调用处可以看到是有AlarmInstance得到的,比如在setSilentState()中的定时时间是instance.getLowNotificationTime():

  1. public Calendar getLowNotificationTime() {
  2. Calendar calendar = getAlarmTime();
  3. calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET);
  4. return calendar;
  5. }

LOW_NOTIFICATION_HOUR_OFFSET值为-2,也就是在闹铃响之前的两小时那一刻会发这个LOW_NOTIFICATION_STATE的广播出来,AlarmStateManager接收到这个广播处理再转移到下一个。广播的接收在onReciever方法中,

  1. @Override
  2. public void onReceive(final Context context, final Intent intent) {
  3. final PendingResult result = goAsync();
  4. final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
  5. wl.acquire();
  6. AsyncHandler.post(new Runnable() {
  7. @Override
  8. public void run() {
  9. handleIntent(context, intent);
  10. result.finish();
  11. wl.release();
  12. }
  13. });
  14. }
  15. private void handleIntent(Context context, Intent intent) {
  16. final String action = intent.getAction();
  17. Log.v("AlarmStateManager received intent " + intent);
  18. if (CHANGE_STATE_ACTION.equals(action)) {
  19. Uri uri = intent.getData();
  20. AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
  21. AlarmInstance.getId(uri));
  22. if (instance == null) {
  23. // Not a big deal, but it shouldn't happen
  24. Log.e("Can not change state for unknown instance: " + uri);
  25. return;
  26. }
  27. int globalId = getGlobalIntentId(context);
  28. int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1);
  29. int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1);
  30. if (intentId != globalId) {
  31. Log.i("Ignoring old Intent. IntentId: " + intentId + " GlobalId: " + globalId +
  32. " AlarmState: " + alarmState);
  33. return;
  34. }
  35. if (alarmState >= 0) {
  36. setAlarmState(context, instance, alarmState);
  37. } else {
  38. registerInstance(context, instance, true);
  39. }
  40. } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) {
  41. Uri uri = intent.getData();
  42. AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
  43. AlarmInstance.getId(uri));
  44. long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
  45. Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId);
  46. viewAlarmIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX);
  47. viewAlarmIntent.putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId);
  48. viewAlarmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  49. context.startActivity(viewAlarmIntent);
  50. setDismissState(context, instance);
  51. }
  52. }
  53. }

在handleIntent方法中统一处理,状态的分发在setAlarmState中:

  1. public void setAlarmState(Context context, AlarmInstance instance, int state) {
  2. switch(state) {
  3. case AlarmInstance.SILENT_STATE:
  4. setSilentState(context, instance);
  5. break;
  6. case AlarmInstance.LOW_NOTIFICATION_STATE:
  7. setLowNotificationState(context, instance);
  8. break;
  9. case AlarmInstance.HIDE_NOTIFICATION_STATE:
  10. setHideNotificationState(context, instance);
  11. break;
  12. case AlarmInstance.HIGH_NOTIFICATION_STATE:
  13. setHighNotificationState(context, instance);
  14. break;
  15. case AlarmInstance.FIRED_STATE:
  16. setFiredState(context, instance);
  17. break;
  18. case AlarmInstance.SNOOZE_STATE:
  19. setSnoozeState(context, instance);
  20. break;
  21. case AlarmInstance.MISSED_STATE:
  22. setMissedState(context, instance);
  23. break;
  24. case AlarmInstance.DISMISSED_STATE:
  25. setDismissState(context, instance);
  26. break;
  27. default:
  28. Log.e("Trying to change to unknown alarm state: " + state);
  29. }
  30. }

对没一个state又转移相应的setXXXState方法中,完成下一次状态的转换,形成一个定时的循环,直到在DISMISSED_STATE里停用或者删除定时项目,如果需要重复则获取下一次定时的时间。

整体的框架就是这样,在AlarmStateManager里使用AlarmManager形成了一个定时的状态机,不断转移到下一个状态处理。

源码在这里https://android.googlesource.com/platform/packages/apps/DeskClock/+/android-4.4.4_r2.0.1

android AlarmManager讲解的更多相关文章

  1. Android开发工程师文集-Android知识点讲解

    前言 大家好,给大家带来Android开发工程师文集-Android知识点讲解的概述,希望你们喜欢 WebView讲解 一般通过Intent调用系统的浏览器: Uri uri = Uri.parse( ...

  2. 四 Android Capabilities讲解

    本文转自:http://www.cnblogs.com/sundalian/p/5629429.html Android Capabilities讲解   1.Capabilities介绍 可以看下之 ...

  3. Android AlarmManager类的应用(实现闹钟功能)

    1.AlarmManager,顾名思义,就是“提醒”,是Android中常用的一种系统级别的提示服务,可以实现从指定时间开始,以一个固定的间隔时间执行某项操作,所以常常与广播(Broadcast)连用 ...

  4. [置顶] Android AlarmManager实现不间断轮询服务

    在消息的获取上是选择轮询还是推送得根据实际的业务需要来技术选型,例如对消息实时性比较高的需求,比如微博新通知或新闻等那就最好是用推送了.但如果只是一般的消息检测比如更新检查,可能是半个小时或一个小时一 ...

  5. Android AlarmManager的取消

    取消alarm使用AlarmManager.cancel()函数,传入参数是个PendingIntent实例. 该函数会将所有跟这个PendingIntent相同的Alarm全部取消,怎么判断两者是否 ...

  6. Android AlarmManager实现不间断轮询服务

    在消息的获取上是选择 轮询还是推送得根据实际的业务需要来技术选型,例如对消息实时性比较高的需求,比如微博新通知或新闻等那就最好是用推送了.但如果只是一般的消息检测比如 更新检查,可能是半个小时或一个小 ...

  7. Android AlarmManager报警的实现

    什么是AlarmManager? AlarmManager它是Android经常使用的系统-Level提醒服务,我们指定为广播中的特定时间Intent. 我们设定一个时间,然后在该时间到来时.Alar ...

  8. android AlarmManager采用

    Android的闹钟实现机制非常easy, 仅仅须要调用AlarmManager.Set()方法将闹钟设置提交给系统,当闹钟时间到后,系统会依照我们的设定发送指定的广播消息.我们写一个广播去接收消息做 ...

  9. 五 Android Capabilities讲解

    1.Capabilities介绍 可以看下之前代码里面设置的capabilities DesiredCapabilities capabilities = new DesiredCapabilitie ...

随机推荐

  1. C#+HtmlAgilityPack+Dapper走一波爬虫

    最近因为公司业务需要,又有机会撸winform了,这次的需求是因为公司有项目申报的这块业务,项目申报前期需要关注政府发布的相关动态信息,政府部门网站过多,人工需要一个一个网站去浏览和查阅,有时候还会遗 ...

  2. 安装MySQL后出现发生系统错误2或者系统找不到指定的文件

    就是出现如下图所示的情况: 上图中画横线的地方可以看出,sql服务确实安装了.出现这种情况的原因就是服务的默认目录与sql文件的安装目录不一致.这里我个人的MySQL安装路径为D:\mysql-5.7 ...

  3. Axis2 webservice入门--Webservice的发布与调用

    一.Webservice发布 参考 http://www.cnblogs.com/demingblog/p/3263576.html 二.webservice 调用 部分参考:http://www.c ...

  4. bootstrap插件fileinput.js 出现出现$("#xxxx").fileinput({}); 不生效的情况解决

    如果出现$("#xxxx").fileinput({}); 不生效的情况请将fileinput.js中最后几行注释掉: /* $(document).ready(function ...

  5. javascrpt_数组学习

    1.构造函数 var arr = new Array(); Array 构造函数有一个很大的缺陷,就是不同的参数,会导致行为不一致. 因此,不建议使用它生成新数组,直接使用字面量是最好的做法. 2.静 ...

  6. AleNet模型笔记

    谁创造了AlexNet? AlexNet是有Hinton大神的弟子Alex Krizhevsky提出的深度卷积神经网络.它可视为LeNet的更深更宽的版本. AlexNet主要用到的技术 成功使用Re ...

  7. jquery插件存档

    1.选择插件selectMenu github地址:https://github.com/josiaho/selectMenu 2.选择插件bootstrap_multiselect 官方地址:htt ...

  8. 四种方式实现子goroutine与主线程的同步

    如何实现子goroutine与主线程的同步 第一种方式: 这种方式很太死板,就不演示了. 第二种方式:使用 channel机制,每个 goroutine传一个 channel进去然后往里写数据,在再主 ...

  9. Goland 提示 :configuration is still incorrect 的解决

    安装好 Goland 后,调试编译的时候提示 goland configuration is still incorrect,百度 和 Google 都没有明确答案 Google 上有一些提示,但是也 ...

  10. centos gnome面板菜单任务栏消失后的解决

    今天终于下决心把我的电脑装成了centos,上网的时候感觉上边的那一行菜单栏碍事儿,就把他给删了 就是桌面最顶上这一行东西,然后百度无果,谷歌上不去,用bing好不容易才找到里解决方案! 症状:进入l ...