在项目开发过程中,有时会有预约提醒、定时提醒等需求,这时我们可以使用系统日历来辅助提醒。通过向系统日历中写入事件、设置提醒方式(闹钟),实现到达某个特定的时间自动提醒的功能。这样做的好处是由于提醒功能是交付给系统日历来做,不会出现应用被杀情况,能够做到准时提醒。
一般来说实现向系统日历中读写事件一般有以下几个步骤:
(1)需要有读写日历权限;
(2)如果没有日历账户需要先创建账户;
(3)实现日历事件增删改查、提醒功能;


1.权限申请

<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />

注意:6.0 以上需要申请权限才可以使用哦

2.日历相关uri

private static String CALENDER_URL = "content://com.android.calendar/calendars";
private static String CALENDER_EVENT_URL = "content://com.android.calendar/events";
private static String CALENDER_REMINDER_URL = "content://com.android.calendar/reminders";

3.具体实现

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.provider.CalendarContract;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;import java.util.Calendar;
import java.util.TimeZone;public class CalendarReminderUtils {private static String CALENDER_URL = "content://com.android.calendar/calendars";private static String CALENDER_EVENT_URL = "content://com.android.calendar/events";private static String CALENDER_REMINDER_URL = "content://com.android.calendar/reminders";private static String CALENDARS_NAME = "boohee";private static String CALENDARS_ACCOUNT_NAME = "BOOHEE@boohee.com";private static String CALENDARS_ACCOUNT_TYPE = "com.android.boohee";private static String CALENDARS_DISPLAY_NAME = "BOOHEE账户";/*** 检查是否已经添加了日历账户,如果没有添加先添加一个日历账户再查询* 获取账户成功返回账户id,否则返回-1*/@RequiresApi(api = Build.VERSION_CODES.N)private static int checkAndAddCalendarAccount(Context context) {int oldId = checkCalendarAccount(context);if( oldId >= 0 ){return oldId;}else{long addId = addCalendarAccount(context);if (addId >= 0) {return checkCalendarAccount(context);} else {return -1;}}}/*** 检查是否存在现有账户,存在则返回账户id,否则返回-1*/private static int checkCalendarAccount(Context context) {Cursor userCursor = context.getContentResolver().query(Uri.parse(CALENDER_URL), null, null, null, null);try {if (userCursor == null) { //查询返回空值return -1;}int count = userCursor.getCount();if (count > 0) { //存在现有账户,取第一个账户的id返回userCursor.moveToFirst();return userCursor.getInt(userCursor.getColumnIndex(CalendarContract.Calendars._ID));} else {return -1;}} finally {if (userCursor != null) {userCursor.close();}}}/*** 添加日历账户,账户创建成功则返回账户id,否则返回-1*/private static long addCalendarAccount(Context context) {TimeZone timeZone = TimeZone.getDefault();ContentValues value = new ContentValues();value.put(CalendarContract.Calendars.NAME, CALENDARS_NAME);value.put(CalendarContract.Calendars.ACCOUNT_NAME, CALENDARS_ACCOUNT_NAME);value.put(CalendarContract.Calendars.ACCOUNT_TYPE, CALENDARS_ACCOUNT_TYPE);value.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, CALENDARS_DISPLAY_NAME);value.put(CalendarContract.Calendars.VISIBLE, 1);value.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.BLUE);value.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER);value.put(CalendarContract.Calendars.SYNC_EVENTS, 1);value.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, timeZone.getID());value.put(CalendarContract.Calendars.OWNER_ACCOUNT, CALENDARS_ACCOUNT_NAME);value.put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0);Uri calendarUri = Uri.parse(CALENDER_URL);calendarUri = calendarUri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, CALENDARS_ACCOUNT_NAME).appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CALENDARS_ACCOUNT_TYPE).build();Uri result = context.getContentResolver().insert(calendarUri, value);long id = result == null ? -1 : ContentUris.parseId(result);return id;}/*** 添加日历事件*/@RequiresApi(api = Build.VERSION_CODES.N)public static void addCalendarEvent(Context context, String title, String description, long reminderTime, int previousDate) {if (context == null) {return;}int calId = checkAndAddCalendarAccount(context); //获取日历账户的idif (calId < 0) { //获取账户id失败直接返回,添加日历事件失败return;}//添加日历事件Calendar mCalendar = Calendar.getInstance();mCalendar.setTimeInMillis(reminderTime);//设置开始时间long start = mCalendar.getTime().getTime();mCalendar.setTimeInMillis(start + 10 * 60 * 1000);//设置终止时间,开始时间加10分钟long end = mCalendar.getTime().getTime();ContentValues event = new ContentValues();event.put("title", title);event.put("description", description);event.put("calendar_id", calId); //插入账户的idevent.put(CalendarContract.Events.DTSTART, start);event.put(CalendarContract.Events.DTEND, end);event.put(CalendarContract.Events.HAS_ALARM, 1);//设置有闹钟提醒event.put(CalendarContract.Events.EVENT_TIMEZONE, "Asia/Shanghai");//这个是时区,必须有Uri newEvent = context.getContentResolver().insert(Uri.parse(CALENDER_EVENT_URL), event); //添加事件if (newEvent == null) { //添加日历事件失败直接返回return;}//事件提醒的设定ContentValues values = new ContentValues();values.put(CalendarContract.Reminders.EVENT_ID, ContentUris.parseId(newEvent));values.put(CalendarContract.Reminders.MINUTES, previousDate * 24 * 60);// 提前previousDate天有提醒values.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);Uri uri = context.getContentResolver().insert(Uri.parse(CALENDER_REMINDER_URL), values);if(uri == null) { //添加事件提醒失败直接返回return;}}/*** 检查日历事件** @param context* @param title*/public static boolean checkCalendarEvent(Context context, String title, String description, long startTime, long endTime) {if (context == null) {return false;}Cursor eventCursor = context.getContentResolver().query(Uri.parse(CALENDER_EVENT_URL), null, null, null, null);try {if (eventCursor == null) { //查询返回空值return false;}if (eventCursor.getCount() > 0) {//遍历所有事件,找到title跟需要查询的title一样的项String eventTitle = "";String eventDescription = "";long eventStartTime;long eventEndTime;while (eventCursor.moveToNext()) {eventTitle = eventCursor.getString(eventCursor.getColumnIndex("title"));eventDescription = eventCursor.getString(eventCursor.getColumnIndex("description"));eventStartTime = Long.parseLong(eventCursor.getString(eventCursor.getColumnIndex("dtstart")));eventEndTime = Long.parseLong(eventCursor.getString(eventCursor.getColumnIndex("dtend")));if ((title != null && title.equals(eventTitle)) && (description != null && description.equals(eventDescription)) && (startTime == eventStartTime) && (endTime == eventEndTime)) {return true;}}}} finally {if (eventCursor != null) {eventCursor.close();}}return false;}/*** 删除日历事件*/public static void deleteCalendarEvent(Context context,String title) {if (context == null) {return;}Cursor eventCursor = context.getContentResolver().query(Uri.parse(CALENDER_EVENT_URL), null, null, null, null);try {if (eventCursor == null) { //查询返回空值return;}if (eventCursor.getCount() > 0) {//遍历所有事件,找到title跟需要查询的title一样的项for (eventCursor.moveToFirst(); !eventCursor.isAfterLast(); eventCursor.moveToNext()) {String eventTitle = eventCursor.getString(eventCursor.getColumnIndex("title"));if (!TextUtils.isEmpty(title) && title.equals(eventTitle)) {int id = eventCursor.getInt(eventCursor.getColumnIndex(CalendarContract.Calendars._ID));//取得idUri deleteUri = ContentUris.withAppendedId(Uri.parse(CALENDER_EVENT_URL), id);int rows = context.getContentResolver().delete(deleteUri, null, null);if (rows == -1) { //事件删除失败return;}}}}} finally {if (eventCursor != null) {eventCursor.close();}}}

4.测试添加事件

 CalendarReminderUtils.addCalendarEvent(this,"学校读书","吃了饭再去",System.currentTimeMillis()+3600*24*1000*2+10000,2);

5,效果


iCalendar Recurrence Rule 规范翻译

规范原文链接:RFC 5545

Recurrence Rule

重复规则 rrule(Recurrence Rule) 属于 icalendar 属性中的一个,配合 dtstart 可以完整描述一个事件的重复行为并计算出重复事件的具体发生 (Occurence)。

重复规则包含多个属性, 每个属性以 NAME = VALUE 对的形式存在, 属性与属性之间用分号区分, 属性之间没有特定的顺序要求,在同一个重复规则中每个属性最多只能出现一次。


FREQ

FREQ 属性表示重复规则的类型, 是重复规则中必须定义的一条属性。 可选的 VALUE 有:

SECONDLY, 表示以秒为间隔单位进行重复。
MINUTELY, 表示以分钟为间隔单位进行重复。
HOURLY, 表示以小时为间隔单位进行重复。
DAILY, 表示以天为间隔单位进行重复。
WEEKLY, 表示以周为间隔单位进行重复。
MONTHLY, 表示以月为间隔单位进行重复。
YEARLY, 表示以年为间隔单位进行重复。

INTERVAL

INTERVAL 属性表示重复规则的间隔, 必须为正整数。 默认值为1, 对应上述不同的 FREQ 值分别表示每一秒,每一分钟, 每一小时, 每一天, 每一周, 每一月, 每一年。

UNTIL

UNTIL 属性定义了一个日期-时间值,用以限制重复规则。 这个日期-时间值表示这个重复规则的最后一次事件的发生时间。 如果重复规则中未包含 UNTIL 和 COUNT 属性, 则表示该重复规则无限重复。

COUNT

COUNT 属性通过定义重复事件的发生次数来限制重复规则。 正整数。

BYSECOND, BYMINUTE, BYHOUR

BYSECOND 取值范围 0 - 59, 可以理解为 “…… 的 n 秒”。
BYMINUTE 取值范围 0 - 59, 可以理解为 “…… 的 n 分”。
BYHOUR 取值范围 0 - 23, 可以理解为 “…… 的 n 时”。

BYDAY

BYDAY 取值范围: MO(周一), TU(周二), WE(周三), TU(周四), FR(周五), SA(周六), SU(周日)。可以有多个值,用逗号分隔。

每个值可以在前面加上一个正整数(+n)或者负整数(-n),用以在 MONTHLY 或者 YEARLY 的重复类型中表示第 n 个周几。 例如,在一个 MONTHLY 类型的重复规则中, +1MO(或者1MO)表示这个月的第1个周一,如果是 -1MO 则表示这个月的最后1个周一。

如果前面没有数字,则表示在这个重复类型中的所有的周几, 比如在一个 MONTHLY 的重复类型中, MO 表示这个月里所有的周一。

BYMONTHDAY

BYMONTHDAY 取值范围 1 - 31 或者 -31 - -1,表示一个月的第几天。 比如, -10 表示一个月的倒数第10天。可以有多个值,用逗号分隔。

BYYEARDAY

BYYEARDAY 取值范围 1 - 366 或者 -366 - -1, 表示一年的第几天。 比如, -1 表示一年的最后一天, 306 表示一年的第306天。可以有多个值,用逗号分隔。

BYWEEKNO

BYWEEKNO 取值范围 1 - 53 或者 -53 - -1, 表示一年的第几周, 只在 YEARLY 类型的重复规则中有效。 比如, 3 表示一年的第 3 周。可以有多个值,用逗号分隔。(注:一年的第一周是指第一个至少包含该年4天时间的那一周)

BYMONTY

BYMONTH 取值范围 1 - 12, 表示一年的第几个月。可以有多个值,用逗号分隔。

WKST

WKST 取值范围 MO, TU, WE, TH, FR, SA, SU。 默认值为 MO。 当一个 WEEKLY 类型的重复规则, INTERVAL 大于 1, 且带有 BYDAY 属性时, 则必须带有 WKST 属性。 当一个 YEARLY 类型的重复规则带有 BYWEEKNO 属性时, 也必须带有 WKST 属性。

BYSETPOS

BYSETPOS 取值范围 1 - 366 或者 -366 - -1, 表示规则指定的事件集合中的第n个事件, 必须与另外的 BYxxx 属性共同使用。 比如,每月的最后一组工作日可以表示为: RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1


如果一个 BYxxx 属性的值超过了它对应的范围,则该属性会被忽略。

当有多个 BYxxx 属性存在的时候, 在代入了 FREQ 和 INTEVAL 属性后,按照以下顺序代入到已有规则上:BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND,BYSETPOS

例如: RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9; BYMINUTE=30

首先,将 INTERVAL=2 代入到 FREQ=YEARLY 上,得到“每2年”, 然后在这基础上代入 BYMONTH=1 得到
“每2年的1月”, 再代入 BYDAY=SU, 得到“每2年的1月的所有周日”, 再代入 BYHOUR=8,9, 得到
“每2年的1月的所有周日的8点9点”(注意是8点和9点,不是8点到9点), 最后代入 BYMINUTE=30, 得到“每2年的1月的所有周日的8点30分9点30分”。

规则中未注明的时间信息,以开始时间(dtstart)为准。


Examples

每天发生一次,重复10次:
RRULE:FREQ=DAILY;COUNT=10

每天发生一次,直到1997年12月24日:
RRULE:FREQ=DAILY;UNTIL=19971224T000000Z

每2天发生一次,直到永远:
RRULE:FREQ=DAILY;INTERVAL=2

每10天发生一次,重复5次:
RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5

当前日期为1998年1月1日9点0分0秒,之后的3年里每年的1月每天发生一次:
RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
或者:
RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1

每周一次,共发生10次:
RRULE:FREQ=WEEKLY;COUNT=10

每周一次,直到1997年12月24日:
RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z

每2周一次, 直到永远:
RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU

当前时间为1997年9月2日9点0分0秒,每周二和周四各发生一次,持续5周:
RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH
或者:
RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH

每周一, 周三, 周五各一次,直到1997年12月24日:
RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR

每2周的周二和周四各发生一次,共发生8次: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH

每月的第一个周五发生一次,共发生10次:
RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR

每月的第一个周五发生一次,直到1997年12月24日:
RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR

每2个月的第一个周日和最后一个周日个发生一次,共发生10次: RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU

每月的倒数第二个周一发生一次,共发生6次:
RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO

每月的倒数第三天发生一次,直到永远:
RRULE:FREQ=MONTHLY;BYMONTHDAY=-3

每月的第2天和第15天各发生一次,共发生10次:
RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15

每月的第1天和最后1天各发生一次,共发生10次:
RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1

每个18个月的1号至15号每天发生一次,共发生10次:
RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15

每2个月的所有周二每天发生一次:
RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU

每年6月和7月各发生一次,共发生10次:
RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7

每2年的一月,二月,三月各发生一次,共10次:
RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3

每3年的第一天,第100天和第200天各发生一次,共10次:
RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200

每年的第20个周一发生一次,直到永远:
RRULE:FREQ=YEARLY;BYDAY=20MO

每年的第20周的周一(以周一为一周起始日)发生一次,直到永远:
RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO

每年3月的所有周四,直到永远:
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH

每年6月,7月,8月的所有周四,直到永远:
RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8

每一个黑色星期五(13号那天为周五)发生一次,直到永远:
RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13

每月第一个周日之后那一周的周六发生一次,直到永远:
RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13

每4年的11月的第一个周一之后的那个周二发生一次,直到永远
RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8

The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months(没法翻译,自己理解):
RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3

每月的倒数第2个工作日,直到永远:
RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2


Android APP 定时提醒

近期研究了下APP中实现定时提醒功能。几经周折算是产出了一个方案。这绝对不是最优的方案,但起码是可用的、相对简单稳定的,希望对大家的实际开发工作有所帮助。

在实现定时提醒的过程中,前前后后考虑过定时推送、系统闹钟、本地定时系统日历的方案。具体的情况将分别简单说一下。

最终技术选型:系统日历

1. 服务器推送

比如京东的Android端APP,经过观察,其走的是后台推送的方案。
这个方案有个前提是:你的APP必须高保活,京东作为超级APP,无论从技术上还是和手机厂商合作上,其保活方案肯定没得说,推送服务的可到达率也毋庸置疑。
假如,你所开发的APP可以有稳定的高保活方案,走后台推送还是不错的。毕竟,app接收到推送通知后,可做的事情太多了,用户体验当然是很好的。

但是,假如你的APP没有做到或做过可靠的长时间高保活,那么,这个方案是不推荐的。APP死掉了,手机收不到推送是没有任何意义的。

(我的理解可能不对,假如京东的工程师们看到了或者对高保活有靠谱方案的同学,还请多都的赐教。)

2. 本地定时

本地定时服务,面临和推送同样的问题,怎么让服务杀不死可以监听到定时。这里不多说了。

3. 系统闹钟

我开始是使用的系统闹钟,本来打算的挺好:设置好定时的闹钟,然后通过APP提前在清单文件中注册好的静态BroadCastReceiver来监听闹钟的系统广播。可是实验发现,这个方案是走不通的或者是我走的姿势不对?

  • 第一:APP调用AlarmMannager来设定的定时是绑定了APP的。什么意思?意思就是,你的app挂了的话,app之前设置的定时闹钟也都被系统清理掉了。
  • 第二:是谁告诉我说通过清单文件静态注册的广播接收者在APP挂了之后还在系统中继续存活监听广播来?坑我不浅啊。

可能是我走路姿势不对?反正这条路在我尝试了一番之后也被我给毙掉了

这是我从网上看到的一篇闹钟的实现方案:http://www.jianshu.com/p/fdb4e8c009b7,尝试了下,发现不管用,而且看作者使用的方法,可能针对的安卓系统版本较早。

贴一下我当初研究闹钟方案时参考的文章:《关于Android中设置闹钟的相对比较完善的解决方案》

4. 系统日历

通过app中设定系统日历的日历事件,并对日历事件设置提醒。不论app是否存活,提醒的时间到了,系统日历总能按时的弹出提醒,唯一的问题是,点击日历的提醒,会进入系统日历的日历事件界面,而无法直接唤醒APP并跳转到相关界面的;系统日历也是没有响应的广播的;

通过从网上搜集资料,我也采用了折中方案:

  • APP设置定时提醒到系统日历(日历的日历事件并设定提醒、描述中填入需要跳转的URL、事件的标题);
  • 定时到达,系统日历主动弹窗或通知栏提醒用户(不同的安卓手机形式不太一样);
  • 用户点击日历提示界面,进入日历事件详情界面
  • 点击日历事件备注中的跳转链接唤起系统选择器;
  • 选择器展示可以处理跳转URL的app
  • 选择浏览器,跳到wap页;选择APP,使用deeplink跳转到相关的原生界面。

4.1 Deeplink

使用系统日历需要使用到的关键技术是Deeplink, 这个大家自己去百度,资料很多,而且不难。
另一个关键的点是:定义deeplink的scheme时,要注意下格式,有的格式系统日历可能不能识别。
推荐大家使用https://开头的,缺点就是系统除了app之外还会唤醒浏览器,需要用户手动选择,加入用户选择了浏览器,还需要一个WAP界面来对应一下。

4.2 代码

下面给出设置系统日历的关键代码:

/*** 作者: Xiao Danchen.* 工具类:* 通过日历添加事件提醒的方式实现秒杀、抢购等提醒功能。* 要求内部实现:* 1,新增提醒是否是重复提醒,是则添加到相关事件下;否则添加到新事件* 2,过期事件、提醒的清理能力** 日历相关的资料:https://developer.android.com/guide/topics/providers/calendar-provider.html?hl=zh-cn#calendar*/
public class CalendarUtils {private static String calanderURL;private static String calanderEventURL;private static String calanderRemiderURL;private static String CALENDARS_NAME = "XXXX";private static String CALENDARS_ACCOUNT_NAME = "XXXX";private static String CALENDARS_ACCOUNT_TYPE = "XXXXX";private static String CALENDARS_DISPLAY_NAME = "XXXXX";/*** 初始化uri*/static {if (Build.VERSION.SDK_INT >= 8) {calanderURL = "content://com.android.calendar/calendars";calanderEventURL = "content://com.android.calendar/events";calanderRemiderURL = "content://com.android.calendar/reminders";} else {calanderURL = "content://calendar/calendars";calanderEventURL = "content://calendar/events";calanderRemiderURL = "content://calendar/reminders";}}/*** 获取日历ID* @param context* @return 日历ID*/private static int checkAndAddCalendarAccounts(Context context){int oldId = checkCalendarAccounts(context);if( oldId >= 0 ){return oldId;}else{long addId = addCalendarAccount(context);if (addId >= 0) {return checkCalendarAccounts(context);} else {return -1;}}}/*** 检查是否存在日历账户* @param context* @return*/private static int checkCalendarAccounts(Context context) {Cursor userCursor = context.getContentResolver().query(Uri.parse(calanderURL), null, null, null, CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL + " ASC ");try {if (userCursor == null)//查询返回空值return -1;int count = userCursor.getCount();if (count > 0) {//存在现有账户,取第一个账户的id返回userCursor.moveToLast();return userCursor.getInt(userCursor.getColumnIndex(CalendarContract.Calendars._ID));} else {return -1;}} finally {if (userCursor != null) {userCursor.close();}}}/*** 添加一个日历账户* @param context* @return*/private static long addCalendarAccount(Context context) {TimeZone timeZone = TimeZone.getDefault();ContentValues value = new ContentValues();value.put(CalendarContract.Calendars.NAME, CALENDARS_NAME);value.put(CalendarContract.Calendars.ACCOUNT_NAME, CALENDARS_ACCOUNT_NAME);value.put(CalendarContract.Calendars.ACCOUNT_TYPE, CALENDARS_ACCOUNT_TYPE);value.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, CALENDARS_DISPLAY_NAME);value.put(CalendarContract.Calendars.VISIBLE, 1);value.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.BLUE);value.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER);value.put(CalendarContract.Calendars.SYNC_EVENTS, 1);value.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, timeZone.getID());value.put(CalendarContract.Calendars.OWNER_ACCOUNT, CALENDARS_ACCOUNT_NAME);value.put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0);Uri calendarUri = Uri.parse(calanderURL);calendarUri = calendarUri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, CALENDARS_ACCOUNT_NAME).appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CALENDARS_ACCOUNT_TYPE).build();Uri result = context.getContentResolver().insert(calendarUri, value);long id = result == null ? -1 : ContentUris.parseId(result);return id;}/*** 向日历中添加一个事件* @param context* @param calendar_id (必须参数)* @param title* @param description* @param begintime 事件开始时间,以从公元纪年开始计算的协调世界时毫秒数表示。 (必须参数)* @param endtime 事件结束时间,以从公元纪年开始计算的协调世界时毫秒数表示。(非重复事件:必须参数)* @return*/private static Uri insertCalendarEvent(Context context, long calendar_id, String title, String description , long begintime, long endtime){ContentValues event = new ContentValues();event.put("title", title);event.put("description", description);// 插入账户的idevent.put("calendar_id", calendar_id);event.put(CalendarContract.Events.DTSTART, begintime);//必须有event.put(CalendarContract.Events.DTEND, endtime);//非重复事件:必须有event.put(CalendarContract.Events.HAS_ALARM, 1);//设置有闹钟提醒event.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());//这个是时区,必须有,//添加事件Uri newEvent = context.getContentResolver().insert(Uri.parse(calanderEventURL), event);return newEvent;}/*** 查询日历事件* @param context* @param title 事件标题* @return 事件id,查询不到则返回""*/private static String queryCalendarEvent(Context context, long calendar_id, String title, String description, long start_time, long end_time){// 根据日期范围构造查询Uri.Builder builder = CalendarContract.Instances.CONTENT_URI.buildUpon();ContentUris.appendId(builder, start_time);ContentUris.appendId(builder, end_time);Cursor cursor = context.getContentResolver().query(builder.build(), null, null, null, null);String tmp_title;String tmp_desc;long temp_calendar_id;if(cursor.moveToFirst()){do{tmp_title = cursor.getString(cursor.getColumnIndex("title"));tmp_desc = cursor.getString(cursor.getColumnIndex("description"));temp_calendar_id = cursor.getLong(cursor.getColumnIndex("calendar_id"));long dtstart = cursor.getLong(cursor.getColumnIndex("dtstart"));if(TextUtils.equals(title,tmp_title) && TextUtils.equals(description,tmp_desc) && calendar_id == temp_calendar_id && dtstart==start_time){String eventId = cursor.getString(cursor.getColumnIndex("event_id"));return eventId;}}while(cursor.moveToNext());}return  "";}/*** 添加日历提醒:标题、描述、开始时间共同标定一个单独的提醒事件* @param context* @param title 日历提醒的标题,不允许为空* @param description 日历的描述(备注)信息* @param begintime 事件开始时间,以从公元纪年开始计算的协调世界时毫秒数表示。* @param endtime 事件结束时间,以从公元纪年开始计算的协调世界时毫秒数表示。* @param remind_minutes 提前remind_minutes分钟发出提醒* @param callback 添加提醒是否成功结果监听*/public static void addCalendarEventRemind(Context context, @NonNull String title, String description, long begintime, long endtime, int remind_minutes, onCalendarRemindListener callback){long calendar_id = checkAndAddCalendarAccounts(context);if(calendar_id < 0){// 获取日历失败直接返回if(null != callback){callback.onFailed(onCalendarRemindListener.Status.CALENDAR_ERR);}return;}//根据标题、描述、开始时间查看提醒事件是否已经存在String event_id = queryCalendarEvent(context,calendar_id,title,description,begintime,endtime);//如果提醒事件不存在,则新建事件if(TextUtils.isEmpty(event_id)){Uri newEvent = insertCalendarEvent(context,calendar_id,title,description,begintime,endtime);if (newEvent == null) {// 添加日历事件失败直接返回if(null != callback){callback.onFailed(onCalendarRemindListener.Status.EVENT_ERROR);}return;}event_id = ContentUris.parseId(newEvent)+"";}//为事件设定提醒ContentValues values = new ContentValues();values.put(CalendarContract.Reminders.EVENT_ID, event_id);// 提前remind_minutes分钟有提醒values.put(CalendarContract.Reminders.MINUTES, remind_minutes);values.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);Uri uri = context.getContentResolver().insert(Uri.parse(calanderRemiderURL), values);if(uri == null) {// 添加提醒失败直接返回if(null != callback){callback.onFailed(onCalendarRemindListener.Status.REMIND_ERROR);}return;}//添加提醒成功if(null != callback){callback.onSuccess();}}/*** 删除日历提醒事件:根据标题、描述和开始时间来定位日历事件* @param context* @param title 提醒的标题* @param description 提醒的描述:deeplink URI* @param startTime 事件的开始时间* @param callback 删除成功与否的监听回调*/public static void deleteCalendarEventRemind(Context context, String title, String description, long startTime,onCalendarRemindListener callback){Cursor eventCursor = context.getContentResolver().query(Uri.parse(calanderEventURL), null, null, null, null);try {if (eventCursor == null)//查询返回空值return;if (eventCursor.getCount() > 0) {//遍历所有事件,找到title、description、startTime跟需要查询的title、descriptio、dtstart一样的项for (eventCursor.moveToFirst(); !eventCursor.isAfterLast(); eventCursor.moveToNext()) {String eventTitle = eventCursor.getString(eventCursor.getColumnIndex("title"));String eventDescription = eventCursor.getString(eventCursor.getColumnIndex("description"));long dtstart = eventCursor.getLong(eventCursor.getColumnIndex("dtstart"));if (!TextUtils.isEmpty(title) && title.equals(eventTitle) && !TextUtils.isEmpty(description) && description.equals(eventDescription) && dtstart==startTime ) {int id = eventCursor.getInt(eventCursor.getColumnIndex(CalendarContract.Calendars._ID));//取得idUri deleteUri = ContentUris.withAppendedId(Uri.parse(calanderEventURL), id);int rows = context.getContentResolver().delete(deleteUri, null, null);if (rows == -1) {// 删除提醒失败直接返回if(null != callback){callback.onFailed(onCalendarRemindListener.Status.REMIND_ERR);}return;}//删除提醒成功if(null != callback){callback.onSuccess();}}}}} finally {if (eventCursor != null) {eventCursor.close();}}}/*** 日历提醒添加成功与否监控器*/public static interface onCalendarRemindListener{enum Status {_CALENDAR_ERROR,_EVENT_ERROR,_REMIND_ERROR}void onFailed(Status error_code);void onSuccess();}/*** 辅助方法:获取设置时间起止时间的对应毫秒数* @param year* @param month 1-12* @param day 1-31* @param hour 0-23* @param minute 0-59* @return*/public static long remindTimeCalculator(int year,int month,int day,int hour,int minute){Calendar calendar = Calendar.getInstance();calendar.set(year,month-1,day,hour,minute);return calendar.getTimeInMillis();}
}

Android向系统日历添加日程提醒事件相关推荐

  1. Android 向系统日历添加日程

    工作需求:需要在某个时间点提前提醒用户秒杀活动开始 由于推送到达率不高不够及时,使用系统日历的日程是最方便最简单的选择 1.使用系统日历需要添加权限 targetSdkVersion=23以上的需要动 ...

  2. android向系统日历添加日程事件(实现闹铃效果,且在app被杀仍能完成)

    向系统日历读写事件有一下步骤 1,有读写日历的权限 2,如果没有日历账户需要先创建日历账户 3,实现日历事件增删改查,提醒功能 一,权限申请 AndroidManifest.xml添加如下权限 < ...

  3. android 添加日程失败,Andriod向系统日历添加日程

    Andriod向系统日历添加日程 1.检查是否有现有的账户存在 private static int checkCalendarAccount(Context context) { Cursor us ...

  4. Android向系统日历添加事件提醒

    项目场景: 在项目开发过程中,需要使用系统日历来辅助提醒.通过向系统日历中写入事件.设置提醒方式,实现到达某个特定的时间自动提醒的功能 解决方案: 1. 请求权限 //Android6.0以上需要动态 ...

  5. 无法同步谷歌日历_苹果日历不能添加日程提醒怎么办?云提醒软件为你罗列待办事项...

    苹果手机上可用来记录日程提醒的软件有提醒事项.备忘录和日历提醒,使用苹果手机的群体会根据个人的喜好选择桌面工具提醒,但是从相关的手机论坛中,网友提出的问题来看,不少人的日历添加日程提醒的按钮呈灰色显示 ...

  6. android 添加日程失败,Android向系统日历中添加日程事件

    总结 在项目开发中,我们有预约提醒.定时提醒需求时,可以使用系统日历来辅助提醒: 通过向系统日历中写入事件.设置提醒方式(闹钟),实现到时间自动提醒的功能: 好处:由于提醒功能是交付给系统日历来做,不 ...

  7. Android向系统日历中添加日程事件

    总结 在项目开发中,我们有预约提醒.定时提醒需求时,可以使用系统日历来辅助提醒: 通过向系统日历中写入事件.设置提醒方式(闹钟),实现到时间自动提醒的功能: 好处:由于提醒功能是交付给系统日历来做,不 ...

  8. Android 在系统日历中添加日程

    在项目开发过程中,有时会有预约提醒.定时提醒等需求,这时我们可以使用系统日历来辅助提醒.通过向系统日历中写入事件.设置提醒方式(闹钟),实现到达某个特定的时间自动提醒的功能.这样做的好处是由于提醒功能 ...

  9. 对Android手机系统日历数据增删改查操作详解

    Android手机系统日历数据增删改查详解 前段时间需要开发提取手机系统的日历数据的功能,自己开始研究了一下,刚开始还是比较懵逼的,经过仔细研究还是能够完全贯通了. 如果不想细细研究,可以直接下载我的 ...

  10. win10计算机日历不能用,win10系统日历应用无法使用新事件的解决方法

    很多小伙伴都遇到过win10系统日历应用无法使用新事件的情况,想必大家都遇到过win10系统日历应用无法使用新事件的情况吧,那么应该怎么处理win10系统日历应用无法使用新事件呢?我们依照1.我们一般 ...

最新文章

  1. HDOJ_ACM_超级楼梯
  2. PostgreSQL: epoch 新纪元时间的使用
  3. 以python入门教程新世界-国外旅行也不忘学习Python:Python 操作列表001
  4. 一名作曲专业毕业生的安全架构师之路
  5. nssl1469-W【dp】
  6. 超硬核!数据库学霸笔记,考试/面试随便秒杀
  7. LeetCode MySQL刷题——day2
  8. libaio源码安装_MySQL5.7.17 编译安装及二进制安装详解
  9. java swing 图片上加热点_外卖图片品牌全靠P,4元成本料理包加热后,平台上20元卖出...
  10. 剑指offer题解 带讲解 python版 第一部分
  11. 微信公众号如何添加附件链接
  12. hgame2021 week2 pwn刷题
  13. 实对称矩阵及其几大性质
  14. 小红书一个月快速涨十万粉的秘籍
  15. 阿尔茨海默病de饮食-微生物-脑轴
  16. 【面试通关篇】13个offer,8家SSP,谈谈我的秋招经验
  17. 2的一百万次方 用计算机算,1M换算:计算机里,单位里1.大家都知道数字1M=10^6对吧(M=Million),以10为底,6次方 2.计算机里1...
  18. 单向流动的拓扑结构_单向流与乱流净化工程原理是什么-百度经验
  19. 计算机组装与拆解中容易混淆的知识点,教资干货 | 教资笔试中易混淆的知识点整合...
  20. 解决远古VOD注入漏洞

热门文章

  1. python中append函数的用法
  2. cccc2016决赛9
  3. macbook打开网页慢解决办法
  4. java中加号_java中加号+的作用
  5. tc android开发工具,TC5.0 (一个脚本开发工具)其底层实现原理分析与推测(半成品)...
  6. 怎样在word中打印框选对√
  7. 如何计算机内存的品牌,如何检查计算机内存模块的品牌?如何检查计算机的内存...
  8. maven-repository文件
  9. windows命令提示符及其操作的相关命令
  10. 网站制作的流程包括哪几个步骤?