美文网首页Android DevAndroid开发程序员
使用辅助服务打造自己的智能视频监控系统

使用辅助服务打造自己的智能视频监控系统

作者: HelloCsl | 来源:发表于2016-10-29 16:16 被阅读9596次

吐槽

最近几个月,家里增加了两位新成员,NIDA和Water,NIDA是一只中华田园猫,是在7月份"妮坦"台风登陆前一晚和女票结缘并收养的,NIDA个性比较凶,喜欢把人的腿当猎物来偷袭和抱腿咬,Water是比NIDA晚来的小金毛,来的时候三个月,好动,喜欢用口去"咬"猫子,把NIDA的玩具占为己有,刚开始还经常偷吃猫粮、水....NIDA也是无可奈何、通常被搞到满是狗子的口水,作为猫星人的尊严呢?但NIDA逃起来,上蹿下跳,Water也只能望尘莫及,可以想象的是,每天下班回来迎接而来的是,几乎被洗劫过的家,还有Water的💩和尿尿(是的,傻狗还没学会在厕所方便呢o(≧口≦)o,汪的一声就哭了),每天上班放这两只东西在家还是有点担忧的,买一个视频监控器,少说也需要个一两百(可以帮它俩买不少零食了),加上自己手上就有一台闲置的碎屏手机(换个屏幕也要100多啊),所以就想要不自己开发一个远程视频监控系统,在需要的时候可以监控一下家里的情况

想法

为了实现视频通讯,使用手机QQ提供的视频电话功能就可以了,足够稳定,所以荒废多年的备用QQ终于可以用上场(不要羡慕我这个有两个QQ的男人),通讯对象分为Client端和Server端,至于通讯模型则是Client端发送特定的命令到Server端,Server端解析Client端的命令,像Client端发起QQ视频聊天,Client端只需要等待并接收视频聊天,最后就可以监控到Server端摄像头的影像,看起来还是SO EASY的,那就开干吧

实现

主要的问题是如何在非人工干预的情况下实现自动化操作,系统的辅助服务功能可以很好的解决这个问题,相信大部分开发者都知道可以用辅助服务来编写微信抢红包插件,具体参见该项目,AccessibilityService的使用也算简单,无非就是监听某种或多种类型事件(通知中心、窗口内容、窗口状态、焦点改变等)的改变,关于AccessibilityService的配置和使用可以看看你真的理解AccessibilityService吗或者直接看官方文档吧,就不在这里唠叨了

状态转换

确定了使用AccessibilityService实现自动化操作的功能后,先来整理一下整个功能的流程或者说场景的转换,见下图:

场景转换.png

可以看出,场景还是不少的,每个场景都需要我们去完成特定的操作,例如在锁屏监听到QQ消息的到来,我们需要检测是不是来自我们的Client(在项目里我以【WaterMonitor:QQ号】为标志,通过在Server修改Client的QQ备注处理,这样的格式也方便获取到需要进行视频电话的QQ联系人),且请求的命令,这些都符合的话,模拟HOME键进入锁屏界面,在锁屏界面还需要模拟上划操作进入解锁界面,并在解锁界面输入正确密码进行解锁,对于这种在不同的场景(状态)的转换并作出相应处理的情景下,我可不想通过If/else来判断当前的状态,并处理,这样大大的增加了程序的耦合性,并且考虑到以后可能在打开QQ的时候,提示登录过期,那我就需要增加一个自动登录的检测和操作,为了解耦,这里使用状态机模式正好,下图是该程序的状态图:

MonitorStateMachine.png

下面简单介绍下各个状态的责任

状态 责任
IdleState 检测Client的命令,并解锁屏幕打开QQ聊天界面
QQChatState 检测是否聊天界面,查找➕号键,点击调出更多功能面板
StartVideoState 检测到视频电话按钮并点击,发起视频通话
EndingSate 通话结束,检测到列表最后一个Item是否是通话结束\取消\拒绝,熄屏重置状态为IdleState

这里就挑IdleState来简单解析下

辅助服务的配置

public class VideoAccessibilityService extends AccessibilityService implements IMonitorService {

  private MonitorState mCurState;

  @Override
  protected void onServiceConnected() {
      super.onServiceConnected();
      registerScreenReceiver();
      AccessibilityServiceInfo info = new AccessibilityServiceInfo();
      info.eventTypes = TYPE_WINDOW_CONTENT_CHANGED | TYPE_WINDOWS_CHANGED | TYPE_WINDOW_STATE_CHANGED | TYPE_NOTIFICATION_STATE_CHANGED;
      info.packageNames = new String[]{Constant.QQ_PKG};
      //...
      this.setServiceInfo(info);
      setState(new IdleState(this));
  }

  @Override
  public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {

    if (mCurState != null) {
        mCurState.handle(accessibilityEvent);
      }
  }

  //...

}

mCurState记录了当前的状态,并在onAccessibilityEvent方法回调的时候交由当前状态去处理事件,onAccessibilityEvent监听的事件在onServiceConnected方法中配置,监听的事件类型TYPE_WINDOW_CONTENT_CHANGED | TYPE_WINDOWS_CHANGED | TYPE_WINDOW_STATE_CHANGED | TYPE_NOTIFICATION_STATE_CHANGED,分别对应了窗口的内容(例如增加某个View),窗口的显示(显示在前台的时候),窗口的状态(Dialog弹出导致窗口失去焦点等)和通知栏状态改变事件类型,监听的包名是QQ的包名,其中通知栏的改变不受包名影响

IdleState的处理


/**
 * 初始状态,等待来电处理
 * change to monitor QQ new message (LockScreen, Notification , QQ App)
 * Created by chensuilun on 16-10-9.
 */
public class IdleState extends MonitorState {
    //...
    @Override
    public void handle(AccessibilityEvent accessibilityEvent) {
        AccessibilityNodeInfo nodeInfo = mContextService.getWindowNode();
        if (nodeInfo == null) {
            return;
        }
        if (isLockScreenMonitorMsg(nodeInfo, accessibilityEvent) || isNotificationMonitorMsg(nodeInfo, accessibilityEvent)) {
            if (AppUtils.isInLockScreen()) {
                // back press
                RootCmd.execRootCmd("input keyevent " + KeyEvent.KEYCODE_BACK);
                // press HOME
                RootCmd.execRootCmd("sleep 0.1 && input keyevent " + KeyEvent.KEYCODE_HOME);
                unlockScreen(nodeInfo);
            }
            final String qqNumber = retrieveQQNumber(nodeInfo, accessibilityEvent);
            mContextService.setState(new QQChatState(mContextService));
            AppApplication.postDelay(new Runnable() {
                @Override
                public void run() {
                    AppUtils.openQQChat(qqNumber);
                }
            }, 1000);
        }
    }

    /**
     * retract monitor cmd from notification
     *
     * @param nodeInfo
     * @param accessibilityEvent
     * @return
     */
    private boolean isNotificationMonitorMsg(AccessibilityNodeInfo nodeInfo, AccessibilityEvent accessibilityEvent) {
        if (accessibilityEvent.getEventType() == TYPE_NOTIFICATION_STATE_CHANGED) {
            Parcelable data = accessibilityEvent.getParcelableData();
            if (data instanceof Notification) {
                if (((Notification) data).tickerText != null) {
                    return (((Notification) data).tickerText.toString().startsWith(MONITOR_TAG)
                            && ((Notification) data).tickerText.toString().endsWith(Constant.MONITOR_CMD_VIDEO));
                }
            }
        }
        return false;
    }

    /**
     * @param nodeInfo
     * @param accessibilityEvent
     * @return If from notification ,msg format :{@link Constant#MONITOR_TAG} + ":real QQ No: "+{@link Constant#MONITOR_CMD_VIDEO}
     */
    private String retrieveQQNumber(AccessibilityNodeInfo nodeInfo, AccessibilityEvent accessibilityEvent) {
        if (accessibilityEvent.getEventType() == TYPE_NOTIFICATION_STATE_CHANGED) {
            Parcelable data = accessibilityEvent.getParcelableData();
            if (data instanceof Notification) {
                if (((Notification) data).tickerText != null) {
                    return ((Notification) data).tickerText.toString().split(":")[1];
                }
            }
        } else {
            List<AccessibilityNodeInfo> nodeInfos = nodeInfo.findAccessibilityNodeInfosByText(MONITOR_TAG);
            if (!AppUtils.isListEmpty(nodeInfos)) {
                String tag;
                for (AccessibilityNodeInfo info : nodeInfos) {
                    tag = (String) info.getText();
                    if (!TextUtils.isEmpty(tag) && tag.contains(MONITOR_TAG)) {
                        return tag.substring(Constant.MONITOR_TAG.length());
                    }
                }
            }
        }
        return Privacy.QQ_NUMBER;
    }

    /**
     * receive monitor cmd in LockScreen
     *
     * @param nodeInfo
     * @param accessibilityEvent
     * @return
     */
    private boolean isLockScreenMonitorMsg(AccessibilityNodeInfo nodeInfo, AccessibilityEvent accessibilityEvent) {
        if (AppUtils.isInLockScreen() && Constant.QQ_PKG.equals(nodeInfo.getPackageName()) && TYPE_WINDOW_CONTENT_CHANGED == accessibilityEvent.getEventType()) {
            if (!AppUtils.isListEmpty(nodeInfo.findAccessibilityNodeInfosByText(MONITOR_TAG))
                    && !AppUtils.isListEmpty(nodeInfo.findAccessibilityNodeInfosByText(Constant.MONITOR_CMD_VIDEO))) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     * @param nodeInfo
     */
    private void unlockScreen(AccessibilityNodeInfo nodeInfo) {
        UnLockUtils.unlock();
    }

}

IdelState处理的是QQ包名的窗口的改变或者通知栏的改变,isLockScreenMonitorMsg在锁屏收到了QQ包名相关的窗口内容改变的事件,通过查找WaterMonitor标志和命令1来决定是否收到了Client端的命令,具体窗口的内容看场景转换图1,isNotificationMonitorMsg则是检测通知栏改变的内容来判断,如果接受到备注为WaterMonitor:111的Client发来的命令1,通过读取通知栏的内容得到的是WaterMonitor:111 1,如果是Client端的视频命令,那么接着判断是否在锁屏,然后解锁,否则就直接查找到联系人的QQ,打开QQ聊天界面并修改状态为QQChatState,接下来的事情就交给QQChatState来处理,这里并没有监听来自QQ主程序的消息列表和聊天面板的新消息,主要是因为比较难判断新来的命令是否已经处理过,但并不影响程序的使用,因为在屏幕熄灭或者聊天结束(EndingSate)的时候都进行了状态的初始化并熄灭屏幕

其他的状态的套路也一样

Root和屏幕解锁

在开发的过程发现,单纯的使用服务服务还是不够的,就是无法进行屏幕解锁,解锁界面大部分都是自定义View实现的,且一般也不支持辅助功能,这是开发中遇到最大的难题,甚至想过如果搞不定锁屏就放弃算了,虽然可以通过禁用安全锁屏来轻松避开这个问题,但对于我来说,是不太能接收这样的限制的,最后为了实现解锁,最后发现通过adb input命令就可以模拟用户按键、触摸等操作,详细的使用可以看这里,我这里稍微解析下我的解锁脚本

sleep 0.1 && input keyevent 3
input swipe 655 1774 655 874
sleep 1 && input tap 612 726
sleep 0.1 && input tap 813 1000
sleep 0.1 && input tap 813 1000
sleep 0.1 && input tap 255 1000
quit

keyevent等于3,代表这是HOME键事件,所以第一行的作用等同于点击HOME键,更多的KEYCODE可以查看android.view.KeyEvent这个类,swipe是滑动操作,即模拟手指从(655,1774)滑动到(655,874),也就是手指上划,主要是进入到解锁界面,tap是点击操作,后面跟的是点击的坐标,所以接下来的四次tap,是模拟点击解锁界面的某些数字,quit是程序本身用于判断脚本结束的标志,并不是adb命令。为了能适配不同的手机,所以把解锁脚本独立出来,放到SD卡根目录,文件名为MonitorUnlock.txt,再根据自己的手机解锁操作,编写好对应的解锁脚本即可,需要解锁的时候就从SD卡中读取该文件

关于是如何确定坐标的,其实很简单,打开开发者模式-指针位置即可查看自己实际操作时候的坐标值

pointer.png

另外为了能够执行adb命令,所以需要Root权限

最后

为了保证程序和QQ能够后台运行,所以记得添加到系统清理的白名单哦,还有如果使用的是国产ROM,最好把程序添加到系统的开启启动项,可以不需要每次重启都手动开启辅助服务
项目已经上传到Github,欢迎Start💕

效果

monitor_compress.gif record1_compress.png record_compress.png

附上两主子帅照

water_compress🐶.png nida_compress🐱.png

相关文章

网友评论

  • 老实李:没想到安卓还能这么玩,太吊了
  • 沐风雨木: 楼主,我也养着狗,嘻嘻。这句代码:isLockScreenMonitorMsg(nodeInfo, accessibilityEvent) || isNotificationMonitorMsg(nodeInfo, accessibilityEvent)我这里执行不进去,导致扣扣消息发送过去,之后只是获取到了,屏幕亮了一下,就没了。
    HelloCsl:@沐风雨木 你可以先自己调试下,看看这两个方法里面执行过程的条件判断哪里出问题了。是不是没有改备注啊?
  • bcf5370e7ba5:请问图案解锁的可以吗
  • 78f7dc0c6c19:为什么魅族mx2 执行 sleep 0.1 && input keyevent 3 没有反应,但是可以执行模拟点击
    HelloCsl:@78f7dc0c6c19 因为keyevent一般都是输入框来接受处理的,而魅族的解锁界面并不是
  • PcDack:好了,好了,我们都知道你有女票😏😏😏
  • 元涛:解锁脚本似乎在小米手机开不了
    元涛:@Stanlyy 嗯嗯,可以了,谢谢。
    Stanlyy:@元涛 小米的也可以了 需要在root权限中给你的应用打开权限。。。。手机root后在去给应用开root权限即可 http://jingyan.baidu.com/article/3065b3b68cfcb6becff8a413.html
    HelloCsl:@往世随疯 所以才要独立出来,自己根据实际解锁方式和密码来修改
  • jessing:我以前的手机倒是root过的,只不过版本是4.1.2,跑不过,现在的手机又舍不得root
    HelloCsl:@jessing 看了下,还是需要用到4.3以后的API,不知道4.3之前有没有可以替代的API呢,你可以尝试下 :yum:
    HelloCsl:@jessing 辅助服务好像2.x时代就有的了,你可以试试改改minSdk看看能不能编译过
    HelloCsl:@jessing ROOT也没啥吧?
  • fbd33b61183d:你好 这个监控 耗电量如何
    HelloCsl: @舞临萌主 都是自己想看的时候才打开,一次不到10分钟,耗电不多,晚上回去了也开飞行模式节省电量,两天一冲
  • XiaLong:请教,请问您是怎么知道类似这样的id的inputBar List<AccessibilityNodeInfo> inputNodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mobileqq:id/inputBar");,还有怎么知道用的什么布局比如这里textNode.getClassName().toString().contains(RelativeLayout.class.getName())
    HelloCsl:@XiaLong uiautomatorviewer
  • 冉冉升起的小太阳:我运行后,具体怎么操作呢,没有反应
    HelloCsl:@冉冉升起的小太阳 WaterMonitor:qq号码,qq号码是要发起视频聊天的QQ号码
    具体看文章
    冉冉升起的小太阳:@HelloCsl qq备注什么 改名字吗?
    HelloCsl:@冉冉升起的小太阳 需要自己配置解锁脚本,QQ备注
  • NANAphei:大神大神,约稿吗?
    NANAphei:我微信是80303489
    NANAphei:@HelloCsl 约本书呀
    HelloCsl:@NANAphei 约啥稿?
  • OliviaChen92:application中s开头的变量是什么缩写呀~没察到
    HelloCsl:@最美是暗恋 静态变量
  • b672175107ff:import github.hellocsl.smartmonitor.utils.Privacy; git上这个类没传
    HelloCsl:@严磊 这个可以自己新建一个,只是记录了默认QQ号码,所以不放上来
  • bhfo:猫耳朵好大!!
  • 涩技师:公猫???
    HelloCsl:@涩技师 母的
  • 左志伟:哈哈

本文标题:使用辅助服务打造自己的智能视频监控系统

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