Android版Prey源码分析
简单点说,Prey的作用就是当你的设备被偷了之后,可以通过安装的Prey应用找到你的设备,网上有成功先例。
这里只简要分析下android版Prey的源码。
各个Activity之间的流程图
我想要知道的重点是怎么激活手机发报告。
手机上一共有两种激活模式,短信激活和控制面板激活。
短信:
App注册了个Receiver用来拦截短信,如果信息包含GO PREY(可设定文字),就会启动Prey进行一系列操作。
<receiver android:name="com.prey.receivers.SmsReceiver"> <intent-filter android:priority="1000"><!--最高优先级--> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter> </receiver>
SmsReceiver的关键代码如下
private void executeActionsBasedOnSMSMessage(Context ctx, String SMSMessage) { PreyConfig preyConfig = PreyConfig.getPreyConfig(ctx); PreyLogger.i("SMS received: " + SMSMessage); boolean shouldPerform = SMSMessage.indexOf(preyConfig.getSmsToRun()) >= 0; boolean shouldStop = SMSMessage.indexOf(preyConfig.getSmsToStop()) >= 0; if (shouldPerform) { PreyLogger.i("SMS Match!, waking up Prey right now!"); abortBroadcast(); //To remove the SMS from the inbox PreyController.startPrey(ctx); } else if (shouldStop) { PreyLogger.i("SMS Match!, stopping Prey!"); abortBroadcast(); //To remove the SMS from the inbox PreyController.stopPrey(ctx); } }
PreyController.startPrey是后续动作的开始,如下所示:
Job的结构图如下:
注意到ActionsRunnner.run()的时候会调用Prey的web service拿到一些配置,比如:
<device> <status> <missing>true</missing> </status> <configuration> <current_release>0.5.9</current_release> <delay>25</delay> <post_url>http://control.preyproject.com/devices/ur_device_id/reports.xml</post_url> <auto_update>false</auto_update> </configuration> <modules> <module type="action" active="true" name="system" version="1.5"/> <module type="report" active="true" name="geo" version="1.7"/> <module type="report" active="true" name="network" version="1.5"/> </modules> </device>
结合Job的结构图和上述XML来说,每个module都会对应一个ActionJob,而每个ActionJob都持有一个PreyAction,这个PreyAction是干实事的。
比如上面的geo对应的是LocationNotifierAction,这个action的作用就是获取设备当前的经纬度。
Job的详细运行过程如下:
PreyController.startPrey会循环检查手机网络是否连通
boolean isPhoneConnected = false; final TelephonyManager tm = (TelephonyManager) ctx.getSystemService(Context.TELEPHONY_SERVICE); final ConnectivityManager connectivityManager = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); try { isPhoneConnected = (tm.getDataState() == TelephonyManager.DATA_CONNECTED) || activeNetInfo.isConnected(); while (!isPhoneConnected) { isPhoneConnected = (tm.getDataState() == TelephonyManager.DATA_CONNECTED) || activeNetInfo.isConnected(); PreyLogger.d("Phone doesn't have internet connection now. Waiting 10 secs for it"); Thread.sleep(10000); } }
如果连通就会start PreyRunnerService(包装Prey的job运行),这个service创建成功之后会调用ActionsRunner.run(),在这个方法里会循环发report,发report是通过调用getInstructionsAndRun实现的。
while (isMissing) { try { isMissing = getInstructionsAndRun(waitNotify, false); preyConfig.setMissing(isMissing); if (isMissing){ PreyRunnerService.interval = preyControlStatus.getDelay(); PreyRunnerService.pausedAt = System.currentTimeMillis(); PreyLogger.d( "Now waiting [" + preyControlStatus.getDelay() + "] minutes before next execution"); Thread.sleep(preyControlStatus.getDelay() * PreyConfig.DELAY_MULTIPLIER); } else PreyLogger.d( "!! Device not marked as missing anymore. Stopping interval execution."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (PreyException e) { //PreyWebServices.getInstance().setMissing(ctx, false); //preyConfig.setMissing(false); //break; PreyLogger.e("Error while running on interval: ",e); } }
在getInstructionsAndRun里会访问Prey的web service得到一些配置(即上面的XML),根据这些配置会创建一些job,并调用ActionsController.getInstance(ctx).runActionGroup(actions,waitNotify,isMissing);来跑这些job。
在这个里面创建了JobsGroup,并将这个JobsGroup加到JobsQueue里,JobsQueue里会去跑JobsGroup。
/** * Adds a JobsGroup to the queue, and starts immediately to execute jobs on * that group. If there was a previously group running, this method finishes that execution first. * * @param jobsGroup * group to add to the queue */ public void addAndRunJobGroup(JobsGroup jobsGroup, Context ctx, boolean isMissing) { this.finishRunningJobs(ctx); this.jobs.put(Long.valueOf(jobsGroup.getId()), jobsGroup); jobsGroup.run(this, isMissing); }
JobsGroup里会去跑每个ActionJob,每个ActionJob持有对一个PreyAction的引用,ActionJob.run()调用PreyAction的execute()。
public void run() { try { this.startedAt = System.currentTimeMillis(); this.action.execute(this, this.ctx); } catch (PreyException e) { PreyLogger.e("Error while running job [" + id + "] :" + e.getMessage(), e); } }
在PreyAction execute的代码里,当这个job跑完之后会调用actionJob.finish(result);继而调用this.jobsGroup.jobFinished(this);在这个方法里会检查这个JobsGroup里每个Job是不是都跑完了。
public void jobFinished(ActionJob job) { this.syncResults.add(job.getResult()); this.runningJobs.remove(Long.valueOf(job.getId())); if (this.runningJobs.isEmpty()) this.queue.groupFinished(this, this.ctx); }
如果跑完会调用this.queue.groupFinished(this, this.ctx);继而调用ActionsController.jobGroupFinished。
public void jobGroupFinished(ArrayList<ActionResult> results, Context ctx) { ArrayList<HttpDataService> dataToBeSent = new ArrayList<HttpDataService>(); if (results.size() > 0) { for (ActionResult aResult : results) { dataToBeSent.add(aResult.getDataToSend()); } PreyWebServices.getInstance().sendPreyHttpReport(ctx, dataToBeSent); } waitNotify.doNotify(); }
在这个方法里会综合每个job的数据并调用Prey的web service发report。
控制面板:
控制面板上设置手机为Missing状态,怎么能让手机知道呢? 答案是通过Google的C2DM(不过现在C2DM已经被废弃了。替代品是GCM),简单讲就是设备需要一个Google账号,在Prey启动的时候,会注册到Google的C2DM server(PreyConfig.getPreyConfig(getApplicationContext()).registerC2dm();),这时Google会返回一个注册的ID,APP拿到之后会访问Prey的web service,web service会将这个ID记下来,当在控制面板上设置手机为Missing状态时,web service会发一个message到Google的service,Google的service接收到之后会根据之前注册的ID,定位到你的手机,并会发intent com.google.android.c2dm.intent.RECEIVE到你的手机。
同样APP端会有一个Receiver处理这个intent。
<!-- Only C2DM servers can send messages for the app. If permission is not set - any other app can generate it --> <receiver android:name="com.prey.receivers.C2DMReceiver" android:permission="com.google.android.c2dm.permission.SEND"> <!-- Receive the actual message --> <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE"/> <category android:name="com.prey"/> </intent-filter> <!-- Receive the registration id --> <intent-filter> <action android:name="com.google.android.c2dm.intent.REGISTRATION"/> <category android:name="com.prey"/> </intent-filter> </receiver>
C2DMReceiver的关键代码如下:
private void handleMessage(Context context, Intent intent) { String pushedMessage = intent.getExtras().getString(PreyConfig.getPreyConfig(context).getc2dmAction()); if (pushedMessage != null) { PreyLogger.i("Push message received " + pushedMessage); try { PushMessage pMessage = new PushMessage(pushedMessage); PreyConfig.getPreyConfig(context).setRunOnce(pMessage.getBody().indexOf("run_once") >= 0); boolean shouldPerform = pushedMessage.indexOf("run") >= 0; boolean shouldStop = pushedMessage.indexOf("stop") >= 0; if (shouldPerform) { PreyLogger.i("Push notification received, waking up Prey right now!"); PreyController.startPrey(context); } else if (shouldStop) { PreyLogger.i("Push notification received, stopping Prey!"); PreyController.stopPrey(context); } } catch (PreyException e) { PreyLogger.e("Push execution failed to run", e); } } }
可以看到除了触发的来源不一样,后续的处理都是一样的。