赶了两天的APP。。。就为了这玩意儿。老早以前就想过用这东西来实现功能的,但是一直有问题。直到前两天有时间了开始着手刷刷这BUG。也算是对这玩意儿的一次重新学习吧。
AlarmManager顾名思义也就是一个提醒管理器,属于Android系统的一项服务,你可以通过这个服务来设置指定时间执行指定的任务。AlarmManager会在你指定的时间启动Activity/发送广播,等等,以完成你需要执行的操作任务。总的来说,这东西就跟闹钟是差不多一个意思的,到了点了然后启动某项你指定的任务
在Simple Netkeeper的编写过程中为了完成后台拨号功能,也用到了这玩意儿。由于是后台操作,因此我选择了使用广播接收器(BroadcastReceiver)来完成这个操作。基本思路是:用户设置/开机启动设置-》加载任务时间-》设置Alarm,指定时间启动-》Receiver收到消息启动服务。
因为我是用接收器来做这项工作,接收器的定义就成了首要部分。
在Android系统中,BroadcastReceiver用于接收系统发送的广播消息(这东西感觉好像和C++里面的消息机制是差不多的),广播消息又分为几种,在此不做过多的阐述。接收器实例化收到消息后调用onReceive回调方法,提供Context和Intent两项参数。其中Intent是广播发送者所指定的,里面携带了发送者所提供的信息,以及指定的行为(Action),Action是一串字符串。在广播发送过程中为了避免收到部分不需要的广播遭受不必要的干扰,因此设置Action可以很好的避免这种情况。可以通过 intent.getAction()方法来获取发送者所指定的行为。
因此对于广播接收器的任务可以大致认为是:收到广播-》判断行为-》执行操作。
在Simple Netkeeper中执行后台定时任务的接收器代码如下(版本1.3.0.55)
package cn.sunflyer.simplenetkeeper; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.wifi.WifiConfiguration; import android.os.Bundle; import android.os.Handler; import android.os.Message; import java.util.Calendar; import java.util.Hashtable; import java.util.logging.LogRecord; import cn.sunflyer.simplenetkeeper.util.AndroidTools; import cn.sunflyer.simplenetkeeper.util.WifiAdmin; import cn.sunflyer.simpnk.control.ConfigController; import cn.sunflyer.simpnk.control.DialController; import cn.sunflyer.simpnk.control.Log; import cn.sunflyer.simpnk.control.MessageHandler; import cn.sunflyer.simpnk.control.StatusController; import cn.sunflyer.simpnk.obj.RouterMecuryTpNew; /** * Created by 陈耀璇 on 2015/5/25. */ public class BackgroundDialerReceiver extends BroadcastReceiver { /**Action:启动后台拨号服务*/ public static final String BG_DIAL_START = "cn.sunflyer.simplenetkeeper.svc.startService"; /**Action :启动后台心跳服务*/ public static final String BG_HEART_START = "cn.sunflyer.simplenetkeeper.svc.startServiceHeartBeat"; private Context mContext; private WifiAdmin mWifiAdmin ; private Handler mMsgHandler = new Handler(){ private String mLastMessage = ""; @Override public void handleMessage(Message msg){ if(msg != null && msg.getData() != null && !msg.getData().isEmpty()){ switch(msg.getData().getInt(MessageHandler.MSG_DATA_ACTION_CODE)){ case MessageHandler.MSG_ACTION_REFRESH_INFO:{ String pszData = msg.getData().getString(MessageHandler.MSG_DATA_INFO); mLastMessage = ( pszData == null ? "状态数据错误" : pszData ); }break; case MessageHandler.MSG_ACTION_DIAL_COMPLETE:{ Log.log("BGB : 后台拨号处理完毕。上一条结果为 - " + mLastMessage); }break; case MessageHandler.MSG_ACTION_TRACK_COMPLETE:{ Log.log("BGB : 后台状态追踪完毕。上一条结果为 - " + mLastMessage); }break; case MessageHandler.MSG_ACTION_HIDE_BLOCK:{ Log.log("BGB : 拨号处理进程操作完毕,开始启动通知"); Message pMsg = new Message(); Bundle pData = new Bundle(); pData.putInt(MessageHandler.MSG_DATA_ACTION_CODE, MessageHandler.MSG_ACTION_SHOW_NOTIFICATION); pData.putString(MessageHandler.MSG_DATA_INFO, mLastMessage); pData.putString(MessageHandler.MSG_DATA_TITLE, "Simple Netkeeper 后台任务"); pData.putString("icon", (mLastMessage != null && mLastMessage.contains("连接成功") ? "ok" : "error")); pMsg.setData(pData); this.sendMessage(pMsg); }break; case MessageHandler.MSG_ACTION_SHOW_NOTIFICATION:{ Bundle pData = msg.getData(); if(pData != null && mContext != null){ String pTitle = pData.getString(MessageHandler.MSG_DATA_TITLE), pContent = pData.getString(MessageHandler.MSG_DATA_INFO), pIcon = pData.getString("icon"); int pIconVal = (pIcon != null && pIcon.equals("ok")) ? R.drawable.ok : R.drawable.error; Log.log("BGB : 通知栏提示 : " + pContent); AndroidTools.postNotification(mContext,pTitle,pContent,pIconVal); }else Log.log("BGB : 通知栏提示 : 操作取消(数据不存在)"); }break; default: } } } }; private Handler mOriginalHandler = null; /**修改处理器为当前状态的处理器*/ private void setMessageHandler(){ mOriginalHandler = MessageHandler.getAndroidHandler(); MessageHandler.setAndroidHandler(mMsgHandler); } /**恢复原始处理器*/ private void recoverMessageHandler(){ MessageHandler.setAndroidHandler(mOriginalHandler); } @Override public void onReceive(Context context, Intent intent) { this.mContext = context; //修改配置文件位置 this.setConfigPath(context); android.util.Log.d("后台拨号服务程序", "接收到广播请求,action:" + intent.getAction() + ", debug:" + intent.getBooleanExtra(SIGNAL_DEBUG, false)); Log.log("BGB : 后台请求已经发现," + intent.getBooleanExtra(SIGNAL_DEBUG, false)); setMessageHandler(); if(BG_DIAL_START.equals(intent.getAction())){ new Thread(new Runnable() { @Override public void run() { if(!checkWifiState(mContext)){ Log.log("BGB : 没有连接到无线网络,或者尝试连接到无线网络出现错误,因此处理出现错误。有关本次错误报告,请参见日志记录。"); AndroidTools.postNotification(mContext, "操作失败", "连接无线网络出现错误,因此设置失败。", R.drawable.error); return; } if(!checkTimeSet(mContext)){ Log.log("BGB : 时间不在设置范围之内,取消操作,进行下一次设置"); startBackgroundService(mContext,true); return; } if(StatusController.isConfigExists && !"".equals(StatusController.sAccName) && !"".equals(StatusController.sAccPassword)){ Log.log("BGB : 请求后台拨号服务"); DialController.dialRouter(); /** new Thread(new Runnable() { @Override public void run() { DialController.dialRouter(); } }).start(); RouterMecuryTpNew pRouter = new RouterMecuryTpNew(); pRouter.trackLink(); Hashtable<String,String> pState = pRouter.getState(); MessageHandler.sendMessage(MessageHandler.MSG_ACTION_REFRESH_INFO, "连接成功,IP:" + pState.get("ip")); MessageHandler.sendMessage(MessageHandler.MSG_ACTION_TRACK_COMPLETE ,""); **/ }else{ //否则配置不存在取消服务 Log.log("BGB : 后台拨号服务请求失败,配置文件并不存在"); } startBackgroundService(mContext,true); recoverMessageHandler(); } }).start(); }else if(BG_HEART_START.equals(intent.getAction())){ Log.log("BGB : 请求心跳代理服务"); }else{ Log.log("BGB : Required Service Not Found , Current Action is : " + intent.getAction()); } } private boolean checkTimeSet(Context c){ Calendar pCal = Calendar.getInstance(); if(pCal != null){ int pDay = pCal.get(Calendar.DAY_OF_WEEK) - 1 ; return pDay >= StatusController.sStartDay && pDay <= StatusController.sEndDay; } return false; } private boolean checkWifiState(Context c){ if(AndroidTools.isWifiNetwork(c)){ String pWifiName = AndroidTools.getWifiName(c); Log.log("BGB : 无线网络状态检查 - 已连接到 " + pWifiName + " , 原始设置要求的无线网络为 " + StatusController.sOntimeWifi); return StatusController.sOntimeWifi != null && !StatusController.sOntimeWifi.equals("") && StatusController.sOntimeWifi.equals(pWifiName); }else{ //连接到指定WIFI if(this.mWifiAdmin == null) this.mWifiAdmin = new WifiAdmin(c); this.mWifiAdmin.openWifi(); //创建WIFI配置文件,强制WPA/WPA2 WifiConfiguration pTargetWifiConf = this.mWifiAdmin.createWifiInfo(StatusController.sOntimeWifi , StatusController.sOntimeWifiKey , WifiAdmin.AUTH_WPA); if(pTargetWifiConf != null){ Log.log("BGB : 连接到无线网络 - 配置生成完毕。"); //等待5秒返回,避免WIFI没有连接上出现问题 boolean pRes = this.mWifiAdmin.addNetwork(pTargetWifiConf); try { Thread.sleep(5000); } catch (InterruptedException e) { Log.log("BGB : 连接到无线网络 - 等待出现错误"); } return pRes; } Log.log("BGB : 连接到无线网络 - 配置生成出现异常,请检查输入内容。(SSID:" + StatusController.sOntimeWifi + ",密码长度:" + StatusController.sOntimeWifiKey.length() + "/需求最低长度为8"); return false; } } private void setConfigPath(Context c){ ConfigController.setConfigPath(c.getFilesDir().toString()); //加载配置文件状态 StatusController.initStatus(c.getFilesDir().toString()); } public static void startBackgroundService(Context c){ startBackgroundService(c,false); } /**启动后台服务*/ public static void startBackgroundService(Context c,boolean secday){ AlarmManager pAlarmMgr = (AlarmManager)c.getSystemService(c.ALARM_SERVICE); Intent pTargetIntent = new Intent(c,BackgroundDialerReceiver.class); //TODO:for debug , DO NOT MODIFY THE FOLLOWING CODE pTargetIntent.putExtra(SIGNAL_DEBUG, true); pTargetIntent.setAction(BackgroundDialerReceiver.BG_DIAL_START); PendingIntent pAlarmInt = PendingIntent.getBroadcast(c , 0 , pTargetIntent , 0); //检查时间选项 Calendar pCal = Calendar.getInstance(); //周日为1,周六为7 int pCurDay = pCal.get(Calendar.DAY_OF_WEEK); //TODO : GO EDIT TIME LOGIC if(pCurDay >= StatusController.sStartDay + 1 && pCurDay <= StatusController.sEndDay + 1 ){ if(pCal.get(Calendar.HOUR_OF_DAY) < StatusController.sStartHour || (pCal.get(Calendar.HOUR_OF_DAY) == StatusController.sStartHour && pCal.get(Calendar.MINUTE) <= StatusController.sStartMin && !secday)){ //默认当天 cn.sunflyer.simpnk.control.Log.log("定时设置 : 设置为当天的 " + StatusController.sStartHour + ":" + StatusController.sStartMin); }else{ //推迟至第二天 pCal.add(Calendar.DAY_OF_MONTH , 1); cn.sunflyer.simpnk.control.Log.log("定时设置 : 设置为次日的 " + StatusController.sStartHour + ":" + StatusController.sStartMin); } }else{ //推迟天数 pCurDay = 7 + StatusController.sStartDay + 1 - pCurDay; pCal.add(Calendar.DAY_OF_MONTH, pCurDay); } pCal.set(pCal.get(Calendar.YEAR), pCal.get(Calendar.MONTH), pCal.get(Calendar.DAY_OF_MONTH), StatusController.sStartHour, StatusController.sStartMin, 0); Log.log("定时设置 : 取得的最终时间为 " + pCal.getTime().toString()); pAlarmMgr.set(AlarmManager.RTC_WAKEUP, pCal.getTimeInMillis(), pAlarmInt); } private static final String SIGNAL_DEBUG = "sunflyer.debugconfigured"; /**关闭后台服务*/ public static void stopBackgroundService(Context c){ AlarmManager pAlarmMgr = (AlarmManager)c.getSystemService(c.ALARM_SERVICE); Intent pTargetIntent = new Intent(c,BackgroundDialerReceiver.class); PendingIntent pAlarmInt = PendingIntent.getBroadcast(c , 0 , pTargetIntent , 0); pAlarmMgr.cancel(pAlarmInt); } }
(为了方便代码重用,我将启动/停止服务的方法写成了静态方法,以避免过度的重复代码造成维护困难。)
在上述代码中你可以看到我启动后台拨号的流程是:判断行为-》建立新的线程-》判断WIFI状态-》检查时间-》检查配置文件-》执行拨号-》启动下一次服务(startBackgroundService方法中包含了时间判断部分)。
在这里,由于BroadcastReceiver和Activity类似,其中的代码都是在主线程中运行,因此根据开发建议,不应该在主线程中有耗时的行为(比如设备请求/网络请求),因为这种请求可能会导致ANR的发生。我使用了新线程处理任务,但是建议使用AsyncTask来处理异步任务以保证多数情况下的线程安全。
代码码完了,你得让系统知道吧,修改 manifest ,注册接收器
<receiver android:name=".BackgroundDialerReceiver"> <intent-filter> <action android:name="cn.sunflyer.simplenetkeeper.svc.startService"></action> <action android:name="cn.sunflyer.simplenetkeeper.svc.startServiceHeartBeat"></action> </intent-filter> </receiver>
intent-filter起到对广播的一种过滤作用,然而我现在并不是太理解,为了避免误导大众我就不说多的了。
好了,接收器做完了,总该有个方法启动这项任务了吧?还记得大明湖畔的startBackgroundService方法么
啥?不记得了?我再贴一次
/**启动后台服务*/ public static void startBackgroundService(Context c,boolean secday){ AlarmManager pAlarmMgr = (AlarmManager)c.getSystemService(c.ALARM_SERVICE); Intent pTargetIntent = new Intent(c,BackgroundDialerReceiver.class); //TODO:for debug , DO NOT MODIFY THE FOLLOWING CODE pTargetIntent.putExtra(SIGNAL_DEBUG, true); pTargetIntent.setAction(BackgroundDialerReceiver.BG_DIAL_START); PendingIntent pAlarmInt = PendingIntent.getBroadcast(c , 0 , pTargetIntent , 0); //检查时间选项 Calendar pCal = Calendar.getInstance(); //周日为1,周六为7 int pCurDay = pCal.get(Calendar.DAY_OF_WEEK); //TODO : GO EDIT TIME LOGIC if(pCurDay >= StatusController.sStartDay + 1 && pCurDay <= StatusController.sEndDay + 1 ){ if(pCal.get(Calendar.HOUR_OF_DAY) < StatusController.sStartHour || (pCal.get(Calendar.HOUR_OF_DAY) == StatusController.sStartHour && pCal.get(Calendar.MINUTE) <= StatusController.sStartMin && !secday)){ //默认当天 cn.sunflyer.simpnk.control.Log.log("定时设置 : 设置为当天的 " + StatusController.sStartHour + ":" + StatusController.sStartMin); }else{ //推迟至第二天 pCal.add(Calendar.DAY_OF_MONTH , 1); cn.sunflyer.simpnk.control.Log.log("定时设置 : 设置为次日的 " + StatusController.sStartHour + ":" + StatusController.sStartMin); } }else{ //推迟天数 pCurDay = 7 + StatusController.sStartDay + 1 - pCurDay; pCal.add(Calendar.DAY_OF_MONTH, pCurDay); } pCal.set(pCal.get(Calendar.YEAR), pCal.get(Calendar.MONTH), pCal.get(Calendar.DAY_OF_MONTH), StatusController.sStartHour, StatusController.sStartMin, 0); Log.log("定时设置 : 取得的最终时间为 " + pCal.getTime().toString()); pAlarmMgr.set(AlarmManager.RTC_WAKEUP, pCal.getTimeInMillis(), pAlarmInt); }
其实这里面最关键的代码就是前面7行和最后一行的set方法。
首先,获取AlarmManager的实例,之前说过这玩意儿是Android系统的一项服务,因此使用Context的getSystemService(ALARM_SERVICE)来获取。
然后建立一个新的Intent实例,构造方法为( Context ,接收器类),如果需要添加信息的话,使用putExtra方法添加,接收器是可以通过getExtra获取到的,然后是设置Action(之前说过了避免干扰)。
接着建立PendingIntent的实例,并使用getBroadcast方法来获取这个实例。这里要注意:所有对Intent的设置必须发生在建立PendingIntent之前,否则建立PendingIntent之后再修改Intent的数据是不会有任何作用的,这里我就吃过亏 – -,改了半天发现Action是null,结果还有这顺序问题,我也是醉了
最后,用AlarmManager设置你的任务。三个参数分别为 执行模式,时间,PendingIntent。执行模式一般选择RTC_WAKEUP,我记得这个是表示到时间了唤醒设备执行任务。
调用方法设置之后,到了指定时间接收器就会接受到广播,然后执行后面的任务了。
然而我也不知道为什么之前坑了我这么久这会儿这么快就搞完了,草草草草草