前言
近期研究了下APP中實(shí)現(xiàn)定時提醒功能。幾經(jīng)周折算是產(chǎn)出了一個方案乎莉。這絕對不是最優(yōu)的方案琢歇,但起碼是可用的、相對簡單穩(wěn)定的梦鉴,希望對大家的實(shí)際開發(fā)工作有所幫助李茫。喜歡探討Android開發(fā)技術(shù)的同學(xué)可以加學(xué)習(xí)小組QQ群: 193765960。
版權(quán)歸作者所有肥橙,如有轉(zhuǎn)發(fā)魄宏,請注明文章出處:http://www.reibang.com/u/d43d948bef39
在實(shí)現(xiàn)定時提醒的過程中,前前后后考慮過定時推送存筏、系統(tǒng)鬧鐘宠互、本地定時系統(tǒng)日歷的方案。具體的情況將分別簡單說一下椭坚。
最終技術(shù)選型:系統(tǒng)日歷
1. 服務(wù)器推送
比如京東的Android端APP予跌,經(jīng)過觀察,其走的是后臺推送的方案善茎。
這個方案有個前提是:你的APP必須高比幔活,京東作為超級APP,無論從技術(shù)上還是和手機(jī)廠商合作上烁焙,其焙叫希活方案肯定沒得說,推送服務(wù)的可到達(dá)率也毋庸置疑骄蝇。
假如膳殷,你所開發(fā)的APP可以有穩(wěn)定的高保活方案九火,走后臺推送還是不錯的赚窃。畢竟,app接收到推送通知后,可做的事情太多了,用戶體驗(yàn)當(dāng)然是很好的蛇数。
但是,假如你的APP沒有做到或做過可靠的長時間高焙又剩活,那么震叙,這個方案是不推薦的掀鹅。APP死掉了,手機(jī)收不到推送是沒有任何意義的媒楼。
(我的理解可能不對乐尊,假如京東的工程師們看到了或者對高保活有靠譜方案的同學(xué)划址,還請多都的賜教扔嵌。)
2. 本地定時
本地定時服務(wù),面臨和推送同樣的問題夺颤,怎么讓服務(wù)殺不死可以監(jiān)聽到定時痢缎。這里不多說了。
3. 系統(tǒng)鬧鐘
我開始是使用的系統(tǒng)鬧鐘世澜,本來打算的挺好:設(shè)置好定時的鬧鐘独旷,然后通過APP提前在清單文件中注冊好的靜態(tài)BroadCastReceiver來監(jiān)聽鬧鐘的系統(tǒng)廣播×攘眩可是實(shí)驗(yàn)發(fā)現(xiàn)嵌洼,這個方案是走不通的或者是我走的姿勢不對?
- 第一:APP調(diào)用AlarmMannager來設(shè)定的定時是綁定了APP的封恰。什么意思麻养?意思就是,你的app掛了的話诺舔,app之前設(shè)置的定時鬧鐘也都被系統(tǒng)清理掉了鳖昌。
- 第二:是誰告訴我說通過清單文件靜態(tài)注冊的廣播接收者在APP掛了之后還在系統(tǒng)中繼續(xù)存活監(jiān)聽廣播來备畦?坑我不淺啊。
可能是我走路姿勢不對遗遵?反正這條路在我嘗試了一番之后也被我給斃掉了萍恕。
這是我從網(wǎng)上看到的一篇鬧鐘的實(shí)現(xiàn)方案:http://www.reibang.com/p/fdb4e8c009b7,嘗試了下逸嘀,發(fā)現(xiàn)不管用车要,而且看作者使用的方法,可能針對的安卓系統(tǒng)版本較早崭倘。
貼一下我當(dāng)初研究鬧鐘方案時參考的文章:《關(guān)于Android中設(shè)置鬧鐘的相對比較完善的解決方案》
4. 系統(tǒng)日歷
通過app中設(shè)定系統(tǒng)日歷的日歷事件翼岁,并對日歷事件設(shè)置提醒。不論app是否存活司光,提醒的時間到了琅坡,系統(tǒng)日歷總能按時的彈出提醒,唯一的問題是残家,點(diǎn)擊日歷的提醒榆俺,會進(jìn)入系統(tǒng)日歷的日歷事件界面,而無法直接喚醒APP并跳轉(zhuǎn)到相關(guān)界面的坞淮;系統(tǒng)日歷也是沒有響應(yīng)的廣播的茴晋;
通過從網(wǎng)上搜集資料,我也采用了折中方案:
- APP設(shè)置定時提醒到系統(tǒng)日歷(日歷的日歷事件并設(shè)定提醒回窘、描述中填入需要跳轉(zhuǎn)的URL诺擅、事件的標(biāo)題);
- 定時到達(dá)啡直,系統(tǒng)日歷主動彈窗或通知欄提醒用戶(不同的安卓手機(jī)形式不太一樣)烁涌;
- 用戶點(diǎn)擊日歷提示界面,進(jìn)入日歷事件詳情界面
- 點(diǎn)擊日歷事件備注中的跳轉(zhuǎn)鏈接喚起系統(tǒng)選擇器酒觅;
- 選擇器展示可以處理跳轉(zhuǎn)URL的app
- 選擇瀏覽器撮执,跳到wap頁;選擇APP舷丹,使用deeplink跳轉(zhuǎn)到相關(guān)的原生界面二打。
4.1 Deeplink
使用系統(tǒng)日歷需要使用到的關(guān)鍵技術(shù)是Deeplink, 這個大家自己去百度,資料很多掂榔,而且不難继效。
另一個關(guān)鍵的點(diǎn)是:定義deeplink的scheme時,要注意下格式装获,有的格式系統(tǒng)日歷可能不能識別瑞信。
推薦大家使用https://開頭的,缺點(diǎn)就是系統(tǒng)除了app之外還會喚醒瀏覽器穴豫,需要用戶手動選擇凡简,加入用戶選擇了瀏覽器逼友,還需要一個WAP界面來對應(yīng)一下。
4.2 代碼
下面給出設(shè)置系統(tǒng)日歷的關(guān)鍵代碼:
/**
* 作者: Xiao Danchen.
* 工具類:
* 通過日歷添加事件提醒的方式實(shí)現(xiàn)秒殺秤涩、搶購等提醒功能帜乞。
* 要求內(nèi)部實(shí)現(xiàn):
* 1,新增提醒是否是重復(fù)提醒筐眷,是則添加到相關(guān)事件下黎烈;否則添加到新事件
* 2,過期事件匀谣、提醒的清理能力
*
* 日歷相關(guān)的資料: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) {//存在現(xiàn)有賬戶照棋,取第一個賬戶的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 (必須參數(shù))
* @param title
* @param description
* @param begintime 事件開始時間,以從公元紀(jì)年開始計算的協(xié)調(diào)世界時毫秒數(shù)表示武翎。 (必須參數(shù))
* @param endtime 事件結(jié)束時間烈炭,以從公元紀(jì)年開始計算的協(xié)調(diào)世界時毫秒數(shù)表示。(非重復(fù)事件:必須參數(shù))
* @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);//非重復(fù)事件:必須有
event.put(CalendarContract.Events.HAS_ALARM, 1);//設(shè)置有鬧鐘提醒
event.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());//這個是時區(qū)宝恶,必須有符隙,
//添加事件
Uri newEvent = context.getContentResolver().insert(Uri.parse(calanderEventURL), event);
return newEvent;
}
/**
* 查詢?nèi)諝v事件
* @param context
* @param title 事件標(biāo)題
* @return 事件id,查詢不到則返回""
*/
private static String queryCalendarEvent(Context context, long calendar_id, String title, String description, long start_time, long end_time){
// 根據(jù)日期范圍構(gòu)造查詢
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 "";
}
/**
* 添加日歷提醒:標(biāo)題、描述垫毙、開始時間共同標(biāo)定一個單獨(dú)的提醒事件
* @param context
* @param title 日歷提醒的標(biāo)題,不允許為空
* @param description 日歷的描述(備注)信息
* @param begintime 事件開始時間霹疫,以從公元紀(jì)年開始計算的協(xié)調(diào)世界時毫秒數(shù)表示。
* @param endtime 事件結(jié)束時間露久,以從公元紀(jì)年開始計算的協(xié)調(diào)世界時毫秒數(shù)表示更米。
* @param remind_minutes 提前remind_minutes分鐘發(fā)出提醒
* @param callback 添加提醒是否成功結(jié)果監(jiān)聽
*/
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;
}
//根據(jù)標(biāo)題、描述毫痕、開始時間查看提醒事件是否已經(jīng)存在
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)+"";
}
//為事件設(shè)定提醒
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();
}
}
/**
* 刪除日歷提醒事件:根據(jù)標(biāo)題、描述和開始時間來定位日歷事件
* @param context
* @param title 提醒的標(biāo)題
* @param description 提醒的描述:deeplink URI
* @param startTime 事件的開始時間
* @param callback 刪除成功與否的監(jiān)聽回調(diào)
*/
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一樣的項(xiàng)
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();
}
}
}
/**
* 日歷提醒添加成功與否監(jiān)控器
*/
public static interface onCalendarRemindListener{
enum Status {
_CALENDAR_ERROR,
_EVENT_ERROR,
_REMIND_ERROR
}
void onFailed(Status error_code);
void onSuccess();
}
/**
* 輔助方法:獲取設(shè)置時間起止時間的對應(yīng)毫秒數(shù)
* @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();
}
}
分享、共贏
歡迎大家加入 學(xué)習(xí)小組 QQ群: 193765960