Android APP 定时提醒

前言
近期研究了下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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,596评论 4 59
  • 国庆回家休息七天。七号的晚上,我拖着箱子走在回寝室的路上 。箱子在石头砌成的小路上发出砰砰的响声。一阵清香飘过。好...
    故事好香阅读 341评论 0 1
  • 晨读分享了《感召力》这本书,“感召力”三个字和文章的赢得本能脑,赢得情感脑,赢得逻辑脑三方面的内容让我想起了当初当...
    遇见靖雯阅读 574评论 13 9
  • 我是一只井底之蛙, 望着只有井口那么大的蓝天。 白天,鸟儿在我的头上歌唱, 夜晚,萤火虫把我团团围绕, 我好似国王...
    海王星1984阅读 546评论 6 2