Android APP 定时提醒

作者: 肖丹晨 | 来源:发表于2017-10-29 11:23 被阅读422次

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

    相关文章

      网友评论

        本文标题:Android APP 定时提醒

        本文链接:https://www.haomeiwen.com/subject/cuxzuxtx.html