前言
近期研究了下APP中实现定时提醒功能。几经周折算是产出了一个方案。这绝对不是最优的方案,但起码是可用的、相对简单稳定的,希望对大家的实际开发工作有所帮助。喜欢探讨Android开发技术的同学可以加学习小组QQ群: 193765960。
版权归作者所有,如有转发,请注明文章出处:http://www.jianshu.com/u/d43d948bef39
在实现定时提醒的过程中,前前后后考虑过定时推送、系统闹钟、本地定时系统日历的方案。具体的情况将分别简单说一下。
最终技术选型:系统日历
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();
}
}
分享、共赢
欢迎大家加入 学习小组 QQ群: 193765960
网友评论