Android 7. 1 DeskClock(一)AlarmClockFragment分析

来源:互联网 发布:python的multiply函数 编辑:程序博客网 时间:2024/06/11 06:14

   在DeskClock模块中主要通过四大界面(AlarmClockFragment、ClockFragment、TimerFragment、StopwatchFragment)与用户交互,而此四个界面中常用的为闹钟设置界面(AlarmClocFragment),因此本文章主要讲解AlarmClocFragment的重要代码和流程。本文主要从三大部分讲解:基本数据、闹钟状态转变过程、代码流程

     为什么从这三方面讲解?因为闹钟模块的特点之一是对数据处理操作很频繁,所以了解基本数据是一个基础,而闹钟状态的转变过程是每一个打开闹钟必须做的重要事情,所以是重点,最后加上代码流程,基本上能明白AlarmClocFragment的整个工作过程。

1. 基本数据

     AlarmClocFragment部分打开的闹钟都包含两条基本的数据Alarm和AlarmInstance:Alarm记录的是闹钟的基础数据;AlarmInstance一部分数据来源于Alarm,同时增加一些闹钟状态和AlarmInstance ID值。

     此时肯定会有疑问:那为什么有Alarm,还需要AlarmInstance呢?

     因为AlarmInstance里面记录着闹钟的状态,并且可以随时删掉,此时Alarm可以起到备份作用。有了Alarm基本数据,用户就可以再次打开闹钟,就能再次创建AlarmInstance;如果代码中只设定Alarm数据,不设定AlarmInstance数据,那删除闹钟后就不能再次打开,只能让用户重新创建了,用户体验就不好,而且影响程序的效率。

     再来讨论下Alarm和AlarmInstance中有哪些数据。Alarm和AlarmInstance的表是在ClockDatabaseHelper中创建的,如下所示:

   Alarm Table :  

 private static void createAlarmsTable(SQLiteDatabase db) {

        db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +

                ClockContract.AlarmsColumns._ID + " INTEGER PRIMARY KEY," +

                ClockContract.AlarmsColumns.HOUR + " INTEGER NOT NULL, " +

                ClockContract.AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +

                ClockContract.AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +

                ClockContract.AlarmsColumns.ENABLED+ " INTEGER NOT NULL, " +

                ClockContract.AlarmsColumns.VIBRATE + " INTEGER NOT NULL, " +

                ClockContract.AlarmsColumns.LABEL + " TEXT NOT NULL, " +

                ClockContract.AlarmsColumns.RINGTONE + " TEXT, " +

                ClockContract.AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);");}

 

   AlarmInstance Table:

 private static void createInstanceTable(SQLiteDatabase db) {

        db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +

                ClockContract.InstancesColumns._ID + " INTEGER PRIMARY KEY," +

                ClockContract.InstancesColumns.YEAR + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.MONTH + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.DAY + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.HOUR + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.MINUTES + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.VIBRATE + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.LABEL + " TEXT NOT NULL, " +

                ClockContract.InstancesColumns.RINGTONE + " TEXT, " +

                ClockContract.InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +

                ClockContract.InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +

                    ALARMS_TABLE_NAME + "(" + ClockContract.AlarmsColumns._ID + ") " +

                    "ON UPDATE CASCADE ON DELETE CASCADE" + ");");}

    其中标红的为两表的不同点:Alarm表中DAYS_OF_WEEK记录着一周有几天响铃,它的值由类DaysOfWeek控制,而实际是用16进制值表示:

  * 0x00: no day

    * 0x01: Monday

    * 0x02: Tuesday

         * 0x04: Wednesday

     * 0x08: Thursday

* 0x10: Friday

   * 0x20: Saturday

 * 0x40: Sunday


    所以可以通过移位计算出一周中周几设定了闹钟。ENABLED 表示闹钟是否打开;DELETE_AFTER_USE表示闹钟用过就删除,一般都是false,不过我也不知道什么情况下用此功能;ALARM_STATE表示闹钟实例的状态;ALARM_ID 是AlarmInstance增加的ID值,用于区分Alarm中的ID值。其他的属性值,从字面意思就能明白,我就不多解释了。

    接下来讲讲对这些数据操作的类Alarm.java和AlarmInstance.java。两个类有相似的地方:都封装了闹钟的增删改查操作。而AlarmInstance类还增加了状态转变所需的时长的计算,也就是过多久(代码设定为两小时),闹钟的状态为LOW_NOTIFICATION_STATE,又过多久(代码设定为0.5小时),闹钟的状态变为HIGH_NOTIFICATION_STATE等因为闹钟的状态会随着时间的变化而改变,例如还有两小时就响铃,那么闹钟就进入准备阶段,而此时的相隔两小时又是几点呢?就由AlarmInstance类中的getLowNotificationTime()、getHighNotificationTime()等计算。

  

2. 闹钟状态的转变

    每一个打开的闹钟都记录着当前的状态,闹钟一共有10种状态,闹钟的状态是闹钟的一种属性,此属性是处理闹钟的评判依据,所以至关重要。

    那闹钟的状态为什么会变化呢?

    原因有两个:一个是用户操作,点击了对应的按钮,从而改变了闹钟状态;一个是因为闹钟随着时间变化,需要呈现出不同现象(比如还有两小时闹钟时,会在通知栏通知用户)。所以当用户更新或者创建新闹钟时,闹钟不仅仅更新了当前闹钟的状态,还通过SetSlinceStateSetLowNotificationState等方法中的scheduleInstanceStateChange设置下一时刻闹钟的状态,其中PendingInternet存储着下一时刻状态的值,当时间到了下一时刻(这个时刻可能是上面getLowNotificationTime计算出来的值),通过激活AlarmService,从而调用AlarmStateManager来更新闹钟的状态值。 

    那闹钟的状态是怎么变化的呢?

POWER_OFF_ALARM_STATE

SILENT_STATE

LOW_NOTIFICATION_STATE

HIDE_NOTIFICATION_STATE

HIGH_NOTIFICATION_STATE

SNOOZE_STATE

FIRED_STATE

MISSED_STATE

DISMISSED_STATE

PREDISMISSED_STATE

   首先我解释下各个状态的意思:假定当前时间为X,我设置了两个闹钟(都打开),其时间为Y和ZPOWER_OFF_ALARM_STATE 状态是设置关机闹钟时闹钟的状态,当任何闹钟状态变化时,AlarmStateManager不仅会更新当前闹钟的状态,还会将下一个最近的闹钟(打开状态的)的状态设为关机闹钟状态,防止手机关机后下一个闹钟不生效,也就是当Y闹钟状态变化时,也会设置Z的状态为POWER_OFF_ALARM_STATE;SILENT_STATE是当闹钟时间Y减去当前时间X的值大于2或者小于0(Y的时间小于当前时间)时,Y闹钟的状态设定为SILENT_STATE;LOW_NOTIFICATION_STATE是指闹钟时间Y减去当前时间X小于2且大于0.5小时时,闹钟状态设定为LOW_NOTIFICATION_STATE;HIGH_NOTIFICATION_STATE是当闹钟时间Y减去当前时间X小于0.5且大于0时,闹钟状态设定为HIGH_NOTIFICATION_STATE;HIDE_NOTIFICATION_STATE比较特别,它是在LOW_NOTIFICATION_STATE的基础上,用户划掉通知栏消息时,闹钟状态设定的状态为HIDE_NOTIFICATION_STATE;SNOOZE_STATE指当闹钟响起时,用户点击暂停按钮时的状态;FIRED_STATE是时间到了,闹钟响起声音时的状态;MISSED_STATE是闹钟响起后五分钟的状态(前提是用户不点击关闭或者暂停闹钟),也就是Y+5时间的状态;DISMISSED_STATE是闹钟响起时用户点击关闭按钮时的状态;PREDISMISSED_STATE是在闹钟没有响起时,用户点击立即关闭时的状态;

     而闹钟的状态之间是可以转换的,所以会比较复杂:假如现在时间为X,用户只设定闹钟Y,并且Y-X>2,接下来用户不做任何操作,那么闹钟的状态会这样变化:

SILENT_STATE-->LOW_NOTIFICATION_STATE-->HIGH_NOTIFICATION_STATE-->FIRED_STATE-->MISSED_STATE  

    如果用户在任何阶关闭闹钟,那闹钟的状态可能变为DISMISSED_STATE或者PREDISMISSED_STATE,其中DISMISSED_STATE是在闹钟响起时,用户点击取消或者关闭Button时触发;如果用户在闹钟没响铃前点击通知栏或者闹钟中的“立即关闭”,就触发PREDISMISSED_STATE。

   那两者有何区别?区别在于PREDISMISSED_STATE状态还没结束,到了时间Y,还会自动变为DISMISSED_STATE。

   在LOW_NOTIFICATION_STATE状态时,通知栏会出现通知,但用户划掉通知,那么就会转变为HIDE_NOTIFICATION_STATE。

     几乎每一种状态都有一个特定的方法控制:

SetSlinceState——SILENT_STATE

SetLowNotificationState——LOW_NOTIFICATION_STATE

SetHideNotificationState——HIDE_NOTIFICATION_STATE

SetHighNotificationState——HIGH_NOTIFICATION_STATE

SetFireState——FIRED_STATE

SetSnoozeState——SNOOZE_STATE

SetMissedState——MISSED_STATE

SetPreDismissState——PREDISMISSED_STATE

SetDismissState——DISMISSED_STATE


    在添加或者更新闹钟时,都会进入AlarmStateManager.java中的registerInstance()方法,通过检测闹钟的状态进入到对应的方法中,如果是新闹钟或者更改了闹钟时间,会通过比较闹钟时间与当前时间差(例如currentTime.after(alarmTime)),判断进入哪个状态设置方法中。而SetSlinceState、SetLowNotificationState等这些方法几乎都会做这些操作:1)更新闹钟状态;(2)更新状态栏信息(当然带有MISSED字样的状态是没有通知栏信息的;(3)安排闹钟下一时间的闹钟状态;(4)更新下一个闹钟所有的状态的转变,最后都会变成DISMISSED_STATE,然后通过AlarmService就会进入AlarmStateManager.SetAlarmState方法,从而进入deleteInstanceAndUpdateParent里面取消系统中此闹钟的注册(通过方法cancelScheduledInstanceStateChange),并删除此闹钟的AlarmInstance实例(每个闹钟都有一个AlarmInstance ID,直接调用的AlarmInstance里面的删除方法)。

   要着重说明下SNOOZE_STATEMISSED_STATE、PREDISMISSED_STATE三个状态是如何转变为DISMISSED_STATE的:

   SNOOZE_STATE是指暂停5分钟,然后又会转变为FIRED_STATE,如果用户点击取消闹钟,则变为DISMISSED_STATE;如果用户没点击取消闹钟,那响铃五分钟后变为MISSED_STATE,过12小时后自动变为DISMISSED_STATE。

     MISSED_STATE经过12小时后,自动变为DISMISSED_STATE。

     PREDISMISSED_STATE在原本闹钟响起的时间点,自动变为DISMISSED_STATE。

     备注:上面5分钟或者12小时都是在AlarmInstance里面定义的。

     闹钟中最复杂的状态转变都说清楚了,那么理解代码流程就容易多了。

3. 代码流程

  AlarmClockFragment代码流程包括两大部分:设置闹钟过程和闹铃响铃过程。

  (1)设置闹钟过程流程:

    

     上图中AlarmClockFragment代表闹钟界面,实际与用户接触的视图(比如用户点击了铃声切换按钮)在ExpandedAlarmViewHolder里面,而响应操作在AlarmTimeClickHandler里面,AlarmTimeClickHandler会调用AlarmUpdateHandler类,而AlarmUpdateHandler就是对Alarm和AlarmInstance数据操作的地方。如果用户点击新增闹钟按钮,就会弹出一个时钟圆盘(就是控件TimePickerCompat)给用户设定时间,当用户设定的时间,时间值通过AlarmTimeClickHandler中的processTimeSet方法给新增的闹钟。

   因此用户对闹钟的更新、添加、删除操作,就会进入到AlarmUpdateHandler中asyncUpdateAlarm、asyncAddAlarm、asyncDeleteAlarm,通过异步操作对Alarm和AlarmInstance数据更新,和闹钟状态的更新变化。而闹钟状态的变化都在AlarmStateManager中进行管理。如果是asyncDeleteAlarm删除操作,那么直接会删除Alarm数据,同时利用deleteAllInstances里面的updateNextAlarmInAlarmManager更新下一个闹钟状态;如果是更新或者添加闹钟,那么会进入到AlarmStateManager中registerInstance方法,registerInstance首先会检测闹钟的状态,如果都不满足,就会通过对比闹钟时间和当前时间差值(使用Calendar中的after等方法),判断闹钟进入什么状态,然后进入对应的方法(例如setHighNotificationState)。而setSilentState这些方法如上面所述(2.闹钟状态的转变中所述),都会进行对应的操作,安排系统闹钟状态是通过scheduleInstanceStateChange方法(或者cancelScheduledInstanceStateChange取消闹钟),而scheduleInstanceStateChange方法通过AlarmManager里面的setExact安排系统闹钟。

 AlarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)里面AlarmManager.RTC_WAKEUP表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间;timeInMillis为毫秒值,是long型的整数 表示从1790-1-1 00:00:00到当前时间总共经过的时间的毫秒数;pendingIntent里面有跳转到AlarmService的Internet,也有闹钟需要转化的状态值,在createStateChangeIntent中实现。

     通过AlarmManager的setExact设置系统闹钟后,系统会激发对应的闹钟响应过程,有兴趣的可以去研究,我只讲DeskClock模块里面的内容。

 (2)闹铃响玲过程

     

     在闹钟设置中,已通过AlarmManager设置了系统闹钟,而随着时间的消逝,距离闹钟时间越来越近,闹钟会自动变换状态,而状态的变化值是保存在PendingInternet中的ALARM_STATE_EXTRA里面(并且PendingInternet里面有激发AlarmService,有一条Action为CHANGE_STATE_ACTION)

     到了状态转变的时间时(状态转变时间上面有陈述),AlarmManager就会激活AlarmService,而AlarmService里面会判断里面的Action是为CHANGE_STATE_ACTION,还是STOP_ALARM_ACTION,如果是CHANGE_STATE_ACTION,则先会进入AlarmStateManager中的handleIntent处理,handleIntent里面是更具闹钟的状态,选择进入哪一个SetXXX方法里面(例如状态为LOW_NOTIFICATION_STATE状态时,就会进入SetLowNotificationState方法里面)。其中有一个与设置闹钟不同点——当闹钟是DISMISSED_STATE状态时,就不会转变为其他状态了,会删除AlarmInstance数据,以及取消注册系统闹钟等一系列操作。更新完闹钟后,又进入AlarmService里面的StartAlarm,开始启动响铃时的操作:首先判断是否关机(通过判断属性ro.alarm_boot值),如果是关机或者锁屏,就需要通过AlarmAlertWakeLock获得CPU唤醒锁,再进入AlarmActivity里面;如果开机状态,那么只在通知栏显示AlarmNotification,此时用户可以点击按钮操作,改变闹钟状态。与此同时会调用AlarmKlaxon控制铃声的响起,其实AlarmKlaxon调用了Framwork层里面的Ringtone接口播放音乐。

     总结:

     主要通过三方面讲解了AlarmClockFragment中闹钟如何设置和闹钟如何响起的。因为闹钟的状态很多,有的状态之间可以相互转换,讲解起来容易混淆,所以我只讲解了常用的状态转变流程,用户暂停或者关闭闹钟的流程没详说,但思路都是一样的。

     AlarmClockFragment部分主要类有:ExpandedAlarmViewHolder闹钟详细视图展示;AlarmUpdateHandler闹钟的增删改查;AlarmStateManager闹钟状态的改变和系统闹钟的安排;AlarmService闹钟铃声的响起或停止铃声;AlarmNotifications通知栏信息的显示,以及用户点击后状态的安排。

   设置闹钟主要是通过AlarmStateManager的registerInstance方法,而下一时刻到达时,是通过激活AlarmService,然后调用AlarmStateManager的setAlarmState处理。

     备注:后续会分析DeskClock模块的其他Fragment。




原创粉丝点击