一个android任务提醒程序
需求:
运行建立多个提醒,每个提醒设置一个时间,到达指定时间后跳出提醒窗体
每个提醒有一个执行按钮,点击后保存执行记录,并且当天不再触发提醒窗体
提醒出现后如果任务还没执行,那么需要在30分钟后再次提醒
用户可以关闭全局提醒服务--指闹钟,或针对某个提醒关闭提醒服务,也可以针对某个提醒只关闭当天提醒服务
2个方案
使用的API-17
需要读写SD卡与自启动以及解锁屏幕的权限
A方案:
使用一个"前端服务"--StartFrontServer,在服务里每2分钟跑个任务,这个任务从数据库sqlite读取全部提醒,然后判断那个提醒需要激活,每次也只激活一个
被激活的提醒会更新LastNotifyTime=当前时间,并且在接下来的半个小时内不再触发(如果任务依然没有标记成[已执行]即LastActTime=当天),。
MedicineFrontService类:
1.在启动程序(EMApplication-onCreate方法中)以及收到开机广播或调整时间等时调用下启动服务,这个重载会立即安排个任务
2.runable代码流程参考下文的图片
3.runable只是一个接口不是自己安排线程,这里是关联到主线程调用
4.private Handler handler=new Handler();//不需要编写handler的消息处理代码
5.提醒窗体使用了AlarmAlertWakeLock来在有屏幕锁的情况下显示提醒窗体
public class MedicineFrontService extends Service { private static final Integer ForegroundId=1001; /* * 启动一个前端服务, 在EMApplication中启动 * 这个服务内部有个Handler来每3分钟检测一次是否有要触发的提醒 * 前端服务部容易被回收 * * (non-Javadoc) * @see android.app.Service#onCreate() */ @SuppressLint("NewApi") @Override public void onCreate() { // TODO Auto-generated method stub Log.d(TAG, "onCreate"); Notification.Builder builder = new Notification.Builder (this.getApplicationContext()); //获取一个Notification构造器 Intent nfIntent = new Intent(this, MedicineMainActivity.class); builder.setContentIntent(PendingIntent. getActivity(this, 0, nfIntent, 0)) // 设置PendingIntent .setLargeIcon(BitmapFactory.decodeResource(this.getResources(),R.drawable.ic_launcher)) // 设置下拉列表中的图标(大图标) .setContentTitle("任务提醒服务") // 设置下拉列表里的标题 .setSmallIcon(R.drawable.ic_launcher) // 设置状态栏内的小图标 .setContentText("服务运行中,使用菜单[暂停服务]退出...") // 设置上下文内容 .setWhen(System.currentTimeMillis()); // 设置该通知发生的时间 Notification notification = builder.build(); // 获取构建好的Notification notification.defaults=Notification.DEFAULT_SOUND; //设置为默认的声音 startForeground(ForegroundId, notification); runnable.run(); super.onCreate(); } //===========联合使用Handler与Runable实现类似闹钟的效果 ==== private Handler handler=new Handler();//不需要编写handler的消息处理代码 //Runnable只是一个接口通常需要关联到线程 private Runnable runnable=new Runnable() { @Override public void run() { // TODO Auto-generated method stub //要做的事情 Log.d(TAG, "---Run---"); MNotifyDao dao=new MNotifyDao(getApplicationContext()); List<MNotifyModel> notifies= dao.loadAliveNotifies(); for (MNotifyModel mIt : notifies) { //不需要提醒服务 if(!mIt.isUseNotifyServer()){ continue; } //检测是否设置忽略了 if(mIt.getLastIgnoreTime()!=null){ String actDate = DateUtil.formatShort(mIt .getLastIgnoreTime()); String curDate = DateUtil.formatShort(new Date()); if (actDate.compareToIgnoreCase(curDate) == 0) { continue;//跳到下一个 } } //检测是否执行了今天 if(mIt.getLastActTime()!=null){ String actDate = DateUtil.formatShort(mIt .getLastActTime()); String curDate = DateUtil.formatShort(new Date()); if (actDate.compareToIgnoreCase(curDate) == 0) { continue;//跳到下一个 } } //设定时间小于当前时间 Date curDate=new Date(); String waringTime= DateUtil.formatShort(curDate)+" " +mIt.getWaringTime() +":00"; if( DateUtil.parse(waringTime).after(curDate)){ continue; } //提醒过了需要等30分钟再次提醒 if(mIt.getLastNotifyTime()!=null){ Long diffMicSec=curDate.getTime() -mIt.getLastNotifyTime().getTime(); Log.d(TAG, String.valueOf(diffMicSec)); if(diffMicSec < 1000*60*30){ //小于30分钟 continue; } } mIt.setLastNotifyTime(curDate); dao.update(mIt); //启动提醒窗体 Intent intent = new Intent(getApplicationContext(), MedicineAlarmActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction("NotificationClick"); String note="接收消息:\r\n" +mIt.getMsg() +"\r\n 设定时间:\r\n"+mIt.getWaringTime(); intent.putExtra("Note",note); intent.putExtra("AddTime", DateUtil.formatLong(curDate)); startActivity(intent); break; } //---------间隔时间-------------------- Integer loopInterval=MedicineSetting.getLoopInterval(getApplicationContext()); handler.postDelayed(this, loopInterval*1000*60); } }; //=================End============= @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand()"); // 在API11之后构建Notification的方式 return Service.START_REDELIVER_INTENT; } @Override public void onDestroy() { Log.d(TAG, "onDestroy"); handler.removeCallbacks(runnable); stopForeground(true); super.onDestroy(); } private static final String TAG = "TestMNotify"; @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } }
B方案:
MedicineAlarmUtil类
1.在CUD或者启动程序以及执行了某个任务以及收到开机广播或调整时间等时调用下schedule(ctx),这个重载会立即安排个任务
2.而这个任务会在时间到是启动MedicineAlarmIntentService
3.schedule会同时启动守护服务,cancel会关闭守护服务
public class MedicineAlarmUtil { public static void schedule(Context ctx){ schedule(ctx, getPendingIntent(ctx),Calendar.getInstance().getTimeInMillis()); } public static void schedule(Context ctx,PendingIntent pi,Long triggerAtMillis){ AlarmManager am = (AlarmManager) ctx .getSystemService(Context.ALARM_SERVICE); am.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pi); MedicineSetting.SetNextFireTime(ctx,DateUtil.formatLong(new Date(triggerAtMillis))); startFrontServer(ctx); } public static void cancel(Context ctx) { AlarmManager am = (AlarmManager) ctx .getSystemService(Context.ALARM_SERVICE); am.cancel(getPendingIntent(ctx)); MedicineSetting.SetNextFireTime(ctx, "未安排"); stopFrontServer(ctx); } public static PendingIntent getPendingIntent(Context ctx) { Intent intent = new Intent(ctx, MedicineAlarmIntentService.class); intent.setAction("medicineNotify"); PendingIntent pi = PendingIntent.getService(ctx, 120, intent, Intent.FLAG_ACTIVITY_NEW_TASK); return pi; } //启动前端守护服务服务 private static void startFrontServer(Context ctx){ Intent whiteIntent = new Intent(ctx, MedicineGuardIntentService.class); ctx.startService(whiteIntent); } //关闭前端守护服务 private static void stopFrontServer(Context ctx){ Intent whiteIntent = new Intent(ctx, MedicineGuardIntentService.class); ctx.stopService(whiteIntent); } }
MedicineGuardIntentService类
守护服务的配置
<service android:name="cn.fstudio.alarm.MedicineFrontService" android:enabled="true" android:exported="false" android:process=":white" > </service> <service android:name="cn.fstudio.alarm.MedicineGuardIntentService" android:enabled="true" android:exported="false" android:process=":white" >
守护服务代码,
public class MedicineGuardIntentService extends Service { private static final Integer ForegroundId=1002; /* *方案B守护进程 * * (non-Javadoc) * @see android.app.Service#onCreate() */ @SuppressLint("NewApi") @Override public void onCreate() { // TODO Auto-generated method stub Log.d(TAG, "onCreate"); build(); runnable.run(); super.onCreate(); } @SuppressLint("NewApi") private void build(){ Notification.Builder builder = new Notification.Builder (this.getApplicationContext()); //获取一个Notification构造器 Intent nfIntent = new Intent(this, MedicineMainActivity.class); builder.setContentIntent(PendingIntent. getActivity(this, 0, nfIntent, 0)) // 设置PendingIntent .setLargeIcon(BitmapFactory.decodeResource(this.getResources(),R.drawable.ic_launcher)) // 设置下拉列表中的图标(大图标) .setContentTitle("提醒服务") // 设置下拉列表里的标题 .setSmallIcon(R.drawable.ic_launcher) // 设置状态栏内的小图标 .setContentText("提醒服务守护服务...") // 设置上下文内容 .setWhen(System.currentTimeMillis()); // 设置该通知发生的时间 Notification notification = builder.build(); // 获取构建好的Notification notification.defaults=Notification.DEFAULT_SOUND; //设置为默认的声音 startForeground(ForegroundId, notification); } //===========联合使用Handler与Runable实现类似闹钟的效果 ==== private Handler handler=new Handler();//不需要编写handler的消息处理代码 private volatile boolean firsRun=true; //Runnable只是一个接口通常需要关联到线程 private Runnable runnable=new Runnable() { @Override public void run() { // TODO Auto-generated method stub //要做的事情 Log.d(TAG, "---Run---wait-"); if(firsRun){ firsRun=false; }else { Log.d(TAG, "---Run-Shcedule--"); if (MedicineSetting.IsUseNotifyServier(getApplicationContext())) { MedicineAlarmUtil.schedule(getApplicationContext()); } } //---------间隔时间-------------------- Integer loopInterval=MedicineSetting.getLoopInterval(getApplicationContext()); Integer guardLoopInterval= loopInterval * 5; //守护者进程轮询时间是设置时间乘以5 handler.postDelayed(this, guardLoopInterval*1000*60); //这个方法不会将代码停在这个位置 //所以下面的提示会打印出来 Log.d(TAG, "---Run-not--wait--"); } }; //=================End============= @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand()"); // 在API11之后构建Notification的方式 return Service.START_REDELIVER_INTENT; } @Override public void onDestroy() { Log.d(TAG, "onDestroy"); handler.removeCallbacks(runnable); stopForeground(true); super.onDestroy(); } private static final String TAG = "TestMNotify"; @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } }
1.守护服务启动前端服务,每个interval*5的时间调用一下schedule ,interval是参数设置里的轮询时间间隔
2.守护服务在主配置文件中的声明
3.handler.postDelayed(this, guardLoopInterval*1000*60);
//这个方法不会将代码停在这个位置
//所以下面的提示会打印出来
Log.d(TAG, "---Run-not--wait--");
MedicineAlarmIntentService类
1.这个类计算要触发的任务并形成通知文本激发通知窗口
2.在当前需要激发通知完成后(通过先更新lastNotifyTime逻辑上控制)再次计算下一次调度的时间
3.通过AlarmManager安排调度时间,并真正激发通知窗口--如果有通知文本要发布
4.重点是计算下一个要安排的任务执行时间,通过计算每个任务的下一次调用时间,并且选择近的时间做为下一次任务调度时间
参考上面的流程图与下面代码getSortedList
package cn.fstudio.alarm; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import cn.fstudio.em.MedicineMainActivity; import cn.fstudio.medicine.notify.R; import cn.fstudio.sqlite.dao.MNotifyDao; import cn.fstudio.sqlite.dao.MNotifyModel; import cn.fstudio.util.DateUtil; import android.annotation.SuppressLint; import android.app.IntentService; import android.app.Notification; import android.app.PendingIntent; import android.content.Intent; import android.graphics.BitmapFactory; import android.util.Log; public class MedicineAlarmIntentService extends IntentService { @Override public void onCreate() { // TODO Auto-generated method stub //启动下前端进行,只是看看据说这样不容易被回收 AlarmAlertWakeLock.acquireScreenCpuWakeLock(this); super.onCreate(); } @Override public void onDestroy() { // TODO Auto-generated method stub AlarmAlertWakeLock.releaseCpuLock(); super.onDestroy(); } private static final String TAG = "TestMNotify"; public MedicineAlarmIntentService(String name) { super(name); // TODO Auto-generated constructor stub } public MedicineAlarmIntentService() { super("MedicineAlarmIntentService"); // TODO Auto-generated constructor stub } @Override protected void onHandleIntent(Intent intent) { // TODO Auto-generated method stub Log.d(TAG, "任务执行--onHandleIntent"); //先安排3分钟后任务再次执行的,防止后面代码执行被中断 //如果后面代码正常执行将覆盖这次安排 Calendar calendar= Calendar.getInstance(); calendar.add(Calendar.MINUTE, 3); PendingIntent pi=MedicineAlarmUtil.getPendingIntent(this); MedicineAlarmUtil.schedule(this, pi, calendar.getTimeInMillis()); List<NInfo> list=GetSortedList(); Date now=new Date(); Date nextFireDate=new Date( now.getTime() + 1000*60*60*8); StringBuilder sb=new StringBuilder(); MNotifyDao dao=new MNotifyDao(getApplicationContext()); Boolean needFire=false; for(int i=0;i<list.size();i++){ NInfo it=list.get(i); if(it.fireNow){ sb.append(it.waringTime +"\r\n"); sb.append(it.msg +"\r\n"); sb.append("----------------------\r\n"); //更新最近提醒时间 it.model.setLastNotifyTime(new Date()); dao.update(it.model); needFire=true; } } //再次计算排序后的列表 List<NInfo> nextList=GetSortedList(); if(nextList.size()>0){ nextFireDate=nextList.get(0).nextRunTime; //处理小于3分钟以上的情况 if(nextFireDate.before(new Date())){ nextFireDate=new Date(); } if( Math.abs(nextFireDate.getTime() - now.getTime())<1000*60*3 ){ nextFireDate= new Date((new Date()).getTime() + 1000*60*3); } } //下次执行超过8小 if(nextFireDate.getTime() > (now.getTime() + 1000*60*60*8)){ nextFireDate=new Date(now.getTime() + 1000*60*60*8); } //安排计算后的下次执行任务 MedicineAlarmUtil.schedule(this, pi, nextFireDate.getTime()); if(needFire) fire(sb.toString()); } private void fire(String note){ Intent intent = new Intent(getApplicationContext(), MedicineAlarmActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction("NotificationClick"); intent.putExtra("Note",note); intent.putExtra("AddTime", DateUtil.formatLong(new Date())); startActivity(intent); } private List<NInfo> GetSortedList(){ List<NInfo> list=new ArrayList<NInfo>(); MNotifyDao dao=new MNotifyDao(getApplicationContext()); List<MNotifyModel> notifies= dao.loadAliveNotifies(); Date now=new Date(); for (MNotifyModel mIt : notifies) { NInfo info=new NInfo(); info.fireNow=true; info.recId=mIt.getRecId(); info.msg=mIt.getMsg(); info.waringTime=mIt.getWaringTime(); info.model=mIt; list.add(info); //不需要提醒服务 if(!mIt.isUseNotifyServer()){ //当前毫秒数加8个小时 info.nextRunTime=new Date(now.getTime() + 1000*60*60*8); info.fireNow=false; continue; } //检测是否设置忽略了 if(mIt.getLastIgnoreTime()!=null){ String actDate = DateUtil.formatShort(mIt .getLastIgnoreTime()); String curDate = DateUtil.formatShort(new Date()); if (actDate.compareToIgnoreCase(curDate) == 0) { Date tomorrow=DateUtil.AddDay( DateUtil.parse(DateUtil.formatShort(new Date())+" " +mIt.getWaringTime()+":00" ),1); info.nextRunTime=tomorrow; info.fireNow=false; continue;//跳到下一个 } } //检测是否执行了今天 if(mIt.getLastActTime()!=null){ String actDate = DateUtil.formatShort(mIt .getLastActTime()); String curDate = DateUtil.formatShort(new Date()); if (actDate.compareToIgnoreCase(curDate) == 0) { Date tomorrow=DateUtil.AddDay( DateUtil.parse(DateUtil.formatShort(new Date())+" " +mIt.getWaringTime()+":00" ),1); info.nextRunTime=tomorrow; info.fireNow=false; continue;//跳到下一个 } } //设定时间小于当前时间 String waringTime= DateUtil.formatShort(now)+" " +mIt.getWaringTime() +":00"; if( DateUtil.parse(waringTime).after(now)){ info.nextRunTime=DateUtil.parse(DateUtil.formatShort(new Date())+" " +mIt.getWaringTime()+":00" ); info.fireNow=false; continue; } //提醒过了需要等30分钟再次提醒 String notifyDate = DateUtil.formatShort(mIt.getLastNotifyTime()); String curDate = DateUtil.formatShort(new Date()); //当前时间 info.nextRunTime=now; if(curDate.compareToIgnoreCase(notifyDate)==0){ //是当前时间 Long diffMicSec=now.getTime() -mIt.getLastNotifyTime().getTime(); Log.d(TAG, String.valueOf(diffMicSec)); if(diffMicSec < 1000*60*30){ //最后提醒时间+30分钟 info.nextRunTime=new Date( mIt.getLastNotifyTime().getTime() + 1000*60*30); info.fireNow=false; continue; } } } Collections.sort(list); return list; } class NInfo implements Comparable<NInfo>{ public Integer recId; public Boolean fireNow; public Date nextRunTime; public String msg; public String waringTime; public MNotifyModel model; @Override public int compareTo(NInfo another) { return nextRunTime.compareTo(another.nextRunTime); } } }
Tips:
1.AlarmManager参考:https://blog.csdn.net/lindroid/article/details/83621590
2.使用Calendar (日历)类来计算下次执行的毫秒数,或者加减Day,Minutes...等等
3.PendingIntent pi = PendingIntent.getService(ctx, 0, intent,
Intent.FLAG_ACTIVITY_NEW_TASK); // 0 就是requestCode,如果这个一样的两次任务安排调用,后一个安排会取代前一个
4.IntentService,Service中的OnCreate以及OnDestroy 在创建或销毁时执行一次,多次在Activity,Service中调用startService,OnCreate等方法不会重复执行.
默认情况下Service是单例方式执行的
5.IntentService 这个是在主线程上执行的不需要将一些如网络操作的方法做异步处理,在onHandleIntent中实现要执行的代码,需要提供一个空参数的构造函数并调用Supper("XXX")
6.B方案中,开启一个前台服务,用来避免进行被回收---到底行不行不清楚。。。
7.,提醒窗体使用了AlarmAlertWakeLock来在有屏幕锁的情况下显示提醒窗体。