南锋

南奔万里空,脱死锋镝余

Android应用添加日历提醒功能

功能

在安卓应用里调用系统日历,直接创建一个带提醒的日历事件,甚至不需要跳转到日历界面,只需要获取系统日历的读取权限即可。

需要的权限

AndroidManifest.xml里添加

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

注意: 如果是Android 6.0(API 23)以上,需要动态申请权限。

代码

创建一个CalendarHelper工具类,包含:

  • 获取系统日历账户
  • 自动写入事件
  • 添加提醒
  • 自动处理没有日历账户的情况(可提示用户手动创建)
  • 动态申请权限(当用户拒绝权限时,我这里会弹出一个提示框,提示的内容可以从外部传入,也可以使用默认的。或者你不是使用默认,直接打开系统的设置页面也是可以的(下面屏蔽了这部分的代码))
  • 判断是否已存在相同时间的逻辑,避免重复添加
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
package com.cocos.calender;

import android.app.Activity;
import android.Manifest;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import java.util.Calendar;
import java.util.TimeZone;
import org.json.JSONObject;
import org.json.JSONException;
import android.widget.Toast;

public class CalendarHelper {

private static final String TAG = "CalendarHelper";
/** 用来存放拒绝权限时的提示语 */
private static String denyPermissionMessage = "未获得日历权限,无法添加提醒事件";
/** 日历权限请求码 */
public static final int REQUEST_CALENDAR_PERMISSION = 1010;

/** 临时存储待执行事件 */
private static PendingEvent pendingEvent;

private static class PendingEvent {
String title;
String description;
String location;
long beginTime;
long endTime;
int reminderMinutes;

PendingEvent(String title, String description, String location,
long beginTime, long endTime, int reminderMinutes) {
this.title = title;
this.description = description;
this.location = location;
this.beginTime = beginTime;
this.endTime = endTime;
this.reminderMinutes = reminderMinutes;
}
}

/**
* 检查权限并添加事件(带权限请求)
*/
public static void addEventWithPermission(Activity activity,
String title,
String description,
String location,
long beginTimeMillis,
long endTimeMillis,
int reminderMinutes) {

// 检查日历读写权限
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR)
!= PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {

// 保存事件等待用户授权
pendingEvent = new PendingEvent(title, description, location, beginTimeMillis, endTimeMillis, reminderMinutes);

// 这里可以加解释,但不强制
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.READ_CALENDAR)) {
Log.i(TAG, "需要日历权限来添加提醒事件");
}

// ✅ 直接请求权限(即使用户上次拒绝,这里依旧会再弹一次)
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR},
REQUEST_CALENDAR_PERMISSION);

} else {
// 权限已授权,直接添加
addEvent(activity, title, description, location, beginTimeMillis, endTimeMillis, reminderMinutes);
}
}



/**
* 在 Activity 的 onRequestPermissionsResult 中调用
*/
public static void onRequestPermissionsResultCalendar(Activity activity,
int requestCode,
@NonNull int[] grantResults) {
if (requestCode == REQUEST_CALENDAR_PERMISSION) {
if (grantResults.length >= 2
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {

Log.i(TAG, "日历权限申请成功");

if (pendingEvent != null) {
addEvent(activity,
pendingEvent.title,
pendingEvent.description,
pendingEvent.location,
pendingEvent.beginTime,
pendingEvent.endTime,
pendingEvent.reminderMinutes);
pendingEvent = null;
}

} else {
Log.e(TAG, "用户拒绝了日历权限");
Toast.makeText(activity,
denyPermissionMessage,
Toast.LENGTH_SHORT).show();
// 如果用户永久拒绝,可跳转设置
// if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, android.Manifest.permission.READ_CALENDAR)) {
// Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
// intent.setData(Uri.parse("package:" + activity.getPackageName()));
// activity.startActivity(intent);
// }
}
}
}

/** 获取系统日历账户 ID */
private static long getCalendarAccountId(Context context) {
Cursor userCursor = context.getContentResolver().query(
CalendarContract.Calendars.CONTENT_URI,
new String[]{CalendarContract.Calendars._ID},
null, null, null);

if (userCursor != null) {
try {
if (userCursor.moveToFirst()) {
return userCursor.getLong(0);
}
} finally {
userCursor.close();
}
}
return -1;
}

/** 判断事件是否已存在(避免重复) */
private static boolean isEventAlreadyExists(Context context, String title, long beginTimeMillis) {
long oneMinuteBefore = beginTimeMillis - 60 * 1000;
long oneMinuteAfter = beginTimeMillis + 60 * 1000;

Cursor cursor = context.getContentResolver().query(
CalendarContract.Events.CONTENT_URI,
new String[]{CalendarContract.Events._ID},
CalendarContract.Events.TITLE + "=? AND " +
CalendarContract.Events.DTSTART + ">=? AND " +
CalendarContract.Events.DTSTART + "<=?",
new String[]{title, String.valueOf(oneMinuteBefore), String.valueOf(oneMinuteAfter)},
null
);

if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return true; // 已存在
}
} finally {
cursor.close();
}
}
return false;
}

/** 插入日历事件 + 提醒 */
private static boolean addEvent(Context context,
String title,
String description,
String location,
long beginTimeMillis,
long endTimeMillis,
int reminderMinutes) {
long calId = getCalendarAccountId(context);
if (calId == -1) {
Log.e(TAG, "没有找到系统日历账户,请先在系统日历中添加一个账户");
return false;
}

if (isEventAlreadyExists(context, title, beginTimeMillis)) {
Log.w(TAG, "事件已存在,跳过添加: " + title);
return false;
}

ContentValues eventValues = new ContentValues();
eventValues.put(CalendarContract.Events.CALENDAR_ID, calId);
eventValues.put(CalendarContract.Events.TITLE, TextUtils.isEmpty(title) ? "未命名事件" : title);
eventValues.put(CalendarContract.Events.DESCRIPTION, description);
eventValues.put(CalendarContract.Events.EVENT_LOCATION, location);
eventValues.put(CalendarContract.Events.DTSTART, beginTimeMillis);
eventValues.put(CalendarContract.Events.DTEND, endTimeMillis);
eventValues.put(CalendarContract.Events.HAS_ALARM, 1);
eventValues.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());

Uri newEvent = context.getContentResolver().insert(CalendarContract.Events.CONTENT_URI, eventValues);
if (newEvent == null) {
Log.e(TAG, "插入日历事件失败");
return false;
}

long eventId = ContentUris.parseId(newEvent);

ContentValues reminderValues = new ContentValues();
reminderValues.put(CalendarContract.Reminders.EVENT_ID, eventId);
reminderValues.put(CalendarContract.Reminders.MINUTES, reminderMinutes);
reminderValues.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);

Uri reminderUri = context.getContentResolver().insert(CalendarContract.Reminders.CONTENT_URI, reminderValues);
if (reminderUri == null) {
Log.e(TAG, "插入提醒失败");
return false;
}

Log.i(TAG, "日历事件添加成功,eventId=" + eventId);
return true;
}
public static void creatroCalendarReminder(Context context,String data){
try {
// 将传入的字符串转成 JSON 对象
JSONObject json = new JSONObject(data);

// 从 JSON 中取字段,如果没有就用默认值
String title = json.optString("title", "测试");
String description = json.optString("description", "测试");
String location = json.optString("location", "测试");
int startHour = json.optInt("startHour", 1);
int startMinute = json.optInt("startMinute", 10);
int endHour = json.optInt("endHour", startHour + 1);

Calendar begin = Calendar.getInstance();
begin.add(Calendar.DAY_OF_MONTH, 0); // 哪天开始,Calendar.DAY_OF_MONTH当前时间 + 后面参数值,比如我这里为0,就是今天,如果为1就是明天
begin.set(Calendar.HOUR_OF_DAY, startHour); // 开始的小时,这里是24小时制 startHour的取值范围为0~23
begin.set(Calendar.MINUTE, startMinute); // 开始的分钟

Calendar end = (Calendar) begin.clone();
end.set(Calendar.HOUR_OF_DAY, endHour); // 结束的时间,参数和上面开始时间一样,赋值方式为end.set()

if (context == null) {
Log.e("Calendar", "Context is null");
return;
}

// 添加事件
CalendarHelper.addEventWithPermission(
(Activity) context,
title,
description,
location,
begin.getTimeInMillis(), //事件开始时间的毫秒值
end.getTimeInMillis(), //事件结束时间的毫秒值
5 // 提前5分钟提醒
);
} catch (JSONException e) {
e.printStackTrace();
Log.e("Calendar", "JSON解析失败:" + data);
}
}

/**
* 从外部传提示文本过来
* @param message
*/
public static void setDenyPermissionMessage(String message) {
if (!TextUtils.isEmpty(message)) {
denyPermissionMessage = message;
}
}
}

Activity中的逻辑

先在Activity中引入CalendarHelper类,并调用CalendarHelper.creatroCalendarReminder()方法,传入参数,实现日历添加功能。

1
2
3
4
5
6
7
8
9
10
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// 处理日历权限
if (requestCode == CalendarHelper.REQUEST_CALENDAR_PERMISSION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
CalendarHelper.onRequestPermissionsResultCalendar(this, requestCode, grantResults);
}
}
}

添加创建日历提醒事件和传入提示文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 创建日历提醒事件
* @param data
*/
public static void creatroCalendarReminder(String data){
Context context = AppActivity.getInstance();
CalendarHelper.creatroCalendarReminder(context,data);
}
/**
* 获取读取日历权限被拒绝时的提示文本
* @param str
*/
public static void setDenyPermissionMessage(String str){
CalendarHelper.setDenyPermissionMessage(str);
}
+