在项目开发过程中,有时会有预约提醒、定时提醒等需求,这时我们可以使用系统日历来辅助提醒。通过向系统日历中写入事件、设置提醒方式(闹钟),实现到达某个特定的时间自动提醒的功能。这样做的好处是由于提醒功能是交付给系统日历来做,不会出现应用被杀情况,能够做到准时提醒。
一般来说实现向系统日历中读写事件一般有以下几个步骤:
(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); //获取日历账户的id
if (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); //插入账户的id
event.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));//取得id
Uri 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,效果

Android向系统日历添加日程提醒事件_android


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);
// 插入账户的id
event.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));//取得id
Uri 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();
}
}