美文网首页
微信插件之语音播报

微信插件之语音播报

作者: GenialSir | 来源:发表于2020-08-07 15:58 被阅读0次

    微信插件之语音播报(此项目仅供逆向学习使用)

    本意是为盲人群体做的一款微信语音辅助的小项目,虽然最终没有被启用,但其中涉及到的反编译思想(逆向思维)以及进程间通讯的模块还是对后续项目开发有一定裨益的。

    • 对于Android逆向项目,本项目目前使用的技术是Xposed框架,但是Xposed目前所知是需要手机Root后才可使用。

    • Xposed设计思想是借用JAVA的反射机制来实现的,Hook所需模块来进行修改,从而达到自身需求。像微信机器人、滴滴出行之类的自动抢单等的插件。

    • 在AndroidStudio的gradle中配置如图:

      image
    • 需要手机先装Xposed插件,针对不同机型,有对应模块的安装

    • 项目插件中需要在assets资源目录下创建命名为xposed_init的文件,里面声明好自身插件入口启动类,如图:


      image
    • 既然要做基于微信消息文本播报的语音插件,那么就要即时获取目标APP的即时数据内容,逆向项目最耗时最费力的模块是定位所需的代码模块加调试,因此不像正常需求开发,会有明确技术耗时与定期。定位代码并Hook无误执行一般来说是有些难定位且很耗时的。

      • 若解决微信的聊天文本信息获取的方案,目前可行有两种:
        • 1,通过目标APP的数据库用SQL语句进行数据的即时查找
        • 2,通过Hook微信在通讯时的调用消息的API(不过这块肯定是相对耗时的。所以,此文本播报项目我采用是第一种方案,毕竟时间成本太高的话,导致做出东西也相对意义上大打折扣。)
      • 对于微信的数据库解密方案,本文不做介绍。

    思路与实现

    • 首先在Xposed项目初始化时,实时检测微信进程,从而Hook住微信并在其运行时做对应的Hook处理。

      • CallingTheDog为本插件的入口类,用来初始化检测微信的主进程,以及微信的APP主UI(LauncherUI)的启动监听、数据库Cursor游标对象的获取。
      •   public class CallingTheDog implements IXposedHookLoadPackage {
        
           //Specify the currently required version.
           public static String currentVersion = WE_CHAT_FLAG.VERSION_6_5_4;
           private WeChatLauncherUI weChatLauncherUI;
           private WeChatDBHelper weChatDBHelper;
        
           @Override
          public void handleLoadPackage(XC_LoadPackage.LoadPackageParam LPParam) throws Throwable {
        
           if (APP_PACKAGE_NAME.WE_CHAT.equals(LPParam.packageName)) {
               if (weChatLauncherUI == null) {
                    LoggerUtils.xd("We Chat init.");
                    weChatLauncherUI = new WeChatLauncherUI(LPParam);
               }
        
               toHookWeChatAttach(LPParam);
           }
        }
        
           private void toHookWeChatAttach(final XC_LoadPackage.LoadPackageParam lpParam) {
           findAndHookMethod(Application.class, "attach", Context.class,
                      new XC_MethodHook() {
                         @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                           super.afterHookedMethod(param);
                           if (weChatDBHelper == null) {
                               LoggerUtils.xd("WeChatDBHelper init.");
                               weChatDBHelper = new WeChatDBHelper();
                               weChatDBHelper.init(lpParam);
                            }
                       }
                    });
               }
           }
        
    • 微信的Cursor数据库游标对象获取代码如下

      •   public class WeChatDBHelper {
        
           public static Method method = null;
           public static Object receiver = null;
        
           /**
           * 微信Cursor读写初始化。
           */
           public void init(final XC_LoadPackage.LoadPackageParam    loadPackageParam){
               String targetSqlClass = "";
              if(WE_CHAT_FLAG.VERSION_6_5_4.equals(CallingTheDog.currentVersion)){
                  targetSqlClass = "com.tencent.mm.bg.g";
           }else if(WE_CHAT_FLAG.VERSION_7_0_4.equals(CallingTheDog.currentVersion)){
               targetSqlClass = "com.tencent.mm.bb.g";
            }
            String targetSqlMethod = "rawQuery";
           findAndHookMethod(targetSqlClass, loadPackageParam.classLoader, targetSqlMethod,
               String.class, String[].class, new XC_MethodHook() {
                     @Override
                     protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                          super.beforeHookedMethod(param);
                          //hook数据库连接对象,用于发起数据主动查询
                         if(method == null){
                            method = (Method) param.method;
                            receiver = param.thisObject;
                       }
                  }
        
                  @Override
                  protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                      super.afterHookedMethod(param);
                      //监听所有的消息查询语句
                      Cursor result = (Cursor) param.getResult();
                      String sqlStr = String.valueOf(param.args[0]);
                      if (result != null && result.getCount()>0 && sqlStr.startsWith("select * from message")) {
                          MsgListeners.listenerPath(result, loadPackageParam);
                      }
                  }
              });
          }
          }
        
    • 借用游标对象获取文本信息的监听管理类,根据监听数据库插入的新消息数据来进行即时播报处理

      • public class MsgListeners {
        
          private static Socket mClientSocket;
          private static PrintWriter mClientPrintWriter;
        
          public static void listenerPath(Cursor cursor, XC_LoadPackage.LoadPackageParam loadPackageParam) {
          //主动发送的status=2,接收的为3
            int status = WeChatMessage.getStatus(cursor);
            WeChatMessage.Type msgType = WeChatMessage.getType(cursor);
        
            //建立Client端
            if (mClientPrintWriter == null) {
                  new Thread() {
                     @Override
                     public void run() {
                      connectTCPServer();
                  }
              }.start();
           } else {
            LoggerUtils.xd("mClientPrintWriter is " + mClientPrintWriter);
          }
           switch (msgType) {
            case TEXT_MESSAGE:
               List<TextMessage> textMessages = WeChatMessage.getTextMessage(cursor);
        
                  //初始化监测音量键提供者。
                VolumeProvider volumeProvider = new VolumeProvider(loadPackageParam);
        
                  for (TextMessage textMessage : textMessages) {
                   if (textMessage == null) {
                      continue;
                  }
                  if (textMessage.getCreateTime() > WeChatMessage.lastSend) {
                      WeChatMessage.lastSend = textMessage.getCreateTime();
                      //处理转发的消息
                  }
        
                  SystemClock.sleep(1000);
                  String textContent = QueryWeChatDB.getNickname(textMessage.getFromUser())
                          + " 来消息:  " + textMessage.getContent();
                  LoggerUtils.xd("GenialSir Msg textContent " + textContent);
        
                  if (!TextUtils.isEmpty(textContent) && mClientPrintWriter != null) {
                      mClientPrintWriter.println(textContent);
                      LoggerUtils.xd("GenialSir mClientPrintWriter textContent " + textContent);
                  } else {
                      LoggerUtils.xd("mClientPrintWriter is null.. ");
                      new Thread() {
                          @Override
                          public void run() {
                              connectTCPServer();
                          }
                      }.start();
                  }
              }
              break;
          case VIDEO_MESSAGE:
              break;
          case IMG_MESSAGE:
              break;
          default:
              break;
          }
          }
        }
        
    • 再进行语音播报模块, ** 注意,如果是Android系统>=21(5.0),则直接使用原生API的TextToSpeech即可实现语音播报,若Android系统<21(5.0),则TextToSpeech不支持中文。 **

      • 如果需要支持中文,那首先可以想到使用三方语音API,如讯飞、百度语音等都可以实现,我在初次使用过程中遇到一些意料之外的问题:
        • 语音API的初始化问题,APP的key签名注册问题。显然,这块直接使用微信的Context注册是有问题的。
        • 如果不依赖微信的Context,可以使用自身插件的Context进行一个三方语音注册,我的 TextVoiceHelper 使用的是讯飞语音。但在使用自身插件的Context时,后面又遇到因进程间通讯而导致语音无法播报的问题。
        • 在使用ALDL过程中,直接支持的数据类型如下:(本项目采用的是Socket)
          • 基本数据类型(int、long、char、boolean、double等);
          • String和CharSequence;
          • List:只支持ArrayList,里面每个元素都必须被AIDL支持;
          • Map:只支持HashMap,里面每个元素都必须被AIDL支持,包括key和value;
          • Parcelable:所有实现了Parceable接口的对象;
          • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用(AIDL接口中只支持方法,不支持声明静态常量,这一点区别于传统接口)。
      • 若采用将插件的Context传递给微信进程中来进行初始化讯飞语音API的操作,这个思路整体感觉很矛盾且不清晰,并且Context在多进程情况下也是问题很多。
        • 那么就不用在跨进程传输Context上下功夫,而是直接将聊天数据跨进程传输,从而借助Socket来进行跨进程传输通信,只需在TextVoiceHelper进程内注册的讯飞直接播报即可。也避免了使用微信Context初始化自己注册讯飞的尴尬与TextVoiceHelper在注册讯飞后,获取微信聊天数据遇到进程通讯的问题。
        • 按照上述思路,将微信文本数据监听这块视为Socket的发送端,自身插件注册服务视为Socket接受端,那么整体语音播报处理流程是不是更清晰简洁了呢?
    • 通过将微信与插件分为客户发送端与服务接受端,得到如下的Socket客户端代码:

      •   private static void connectTCPServer() {
        
           Socket socket = null;
           while (socket == null) {
              try {
                socket = new Socket("localhost", 8688);
                mClientSocket = socket;
                mClientPrintWriter = new PrintWriter(new BufferedWriter(
                       new OutputStreamWriter(socket.getOutputStream())), true);
        
               LoggerUtils.xd("genial sir connect tcp server success.");
            } catch (IOException e) {
                e.printStackTrace();
                SystemClock.sleep(1000);
                LoggerUtils.xd("genial sir connect tcp server failed, retry...");
           }
           }
        
          try {
           //接受服务器端的消息
           BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
           while (true) {
               LoggerUtils.xd("genial sir receive br : " + br);
               String msg = br.readLine();
               LoggerUtils.xd("genial sir receive : " + msg);
               if (msg.contains("Close Socket service")) {
                      break;
                  }
           }
           LoggerUtils.xd("genial sir quit...");
           CloseUtils.closeQuietly(mClientPrintWriter);
           CloseUtils.closeQuietly(br);
           socket.close();
        } catch (IOException e) {
           e.printStackTrace();
           }
           }
        
    • Socket服务端初始化与实现:

      •   private void initSocket() {
          //启动Socket服务类
          Intent serviceIntent = new Intent(MainActivity.this, VoiceSocketManager.class);
          startService(serviceIntent);
          }
        
      •   public class VoiceSocketManager extends Service {
        
          private TTSUtils ttsUtils;
          private boolean mIsServiceDestroyed = false;
        
          @Override
          public void onCreate() {
              super.onCreate();
              new Thread(new TcpServer()).start();
          }
        
          @Nullable
          @Override
          public IBinder onBind(Intent intent) {
              return null;
          }
        
        
          private class TcpServer implements Runnable {
        
              @Override
              public void run() {
                  ServerSocket serverSocket;
                  try {
                      //监听本地8688端口
                   serverSocket = new ServerSocket(8688);
                  } catch (IOException e) {
                      LoggerUtils.d("establish tcp server failed, port:8688");
                      e.printStackTrace();
                      return;
                  }
                  while (!mIsServiceDestroyed) {
                      try {
                          //接受客户端请求
                          final Socket client = serverSocket.accept();
                          new Thread() {
                              @Override
                              public void run() {
                                  try {
                                      responseClient(client);
                                  } catch (IOException e) {
                                      e.printStackTrace();
                                  }
                              }
                          }.start();
                      } catch (Exception e) {
                          LoggerUtils.d("error " + e.toString());
                          e.printStackTrace();
                      }
                  }
              }
           }
        
          private void responseClient(Socket client) throws IOException {
              Context applicationContext =         getApplication().getApplicationContext();
              initXF(applicationContext);
              //用于接受客户端消息
              BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
              //用于向客户端发送消息
              PrintWriter printWriter = new PrintWriter(new BufferedWriter(
                      new OutputStreamWriter(client.getOutputStream())), true);
        
        
              String clientContent;
              while (true) {
                  clientContent = in.readLine();
                  LoggerUtils.d("responseClient msg from client: " + clientContent);
                  if ("Close socket".equals(clientContent)) {
                      //客户端断开链接
                      if (ttsUtils != null) {
                          ttsUtils.speak("我是Voice Socket Manager, 客户端请求断开链接,撒哟啦啦");
              }
                      LoggerUtils.d("客户端断开链接.");
                      break;
                  }
                  if (ttsUtils != null) {
                      ttsUtils.speak(clientContent);
                  }else {
                      LoggerUtils.e("ttsUtils is null.");
                  }
              }
              LoggerUtils.d("client quit.");
              //关闭流
              CloseUtils.closeQuietly(printWriter);
              CloseUtils.closeQuietly(in);
              client.close();
          }
        
          private void initXF(Context context) {
              SpeechUtility.createUtility(context, "appid=5d07631c");
              Setting.setShowLog(true);
              ttsUtils = TTSUtils.getInstance(context);
              ttsUtils.init();
          }
        
          @Override
          public void onDestroy() {
              mIsServiceDestroyed = true;
              super.onDestroy();
          }
          }
        
    • 项目TextVoiceHelperGithub地址

    相关文章

      网友评论

          本文标题:微信插件之语音播报

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