【android】从零开始学习安卓录制回放程序制作需要多少天?

更新时间:2022-05-19(第8天)(完结)
终于做完了。

之前没做过安卓的开发,现在突然要搞这个东西,很是头大。
写篇记录性质的博客,看看到底多少天能实现。
期限三个星期,只想弄完赶紧跑路了。

功能需求

在这里插入图片描述
提示可以使用HOOK实现,但是我连HOOK是什么都不知道。。。先从最基本的概念开始吧。

2022-05-19 20:57
终于做完了,效果差不多了,来写总结,放前面。踩坑学习日志就放后面了。

总结

请添加图片描述

项目代码:github: android_replay_test

目标程序 比较简单,就不上 github了。

目标程序:比较简单,主要就是几个组件的编写。
测试程序:在不改动 目标程序 代码的情况下,使用HOOK技术,完成需求。


环境:夜神模拟器7.0.2.7
额外模块:Xposed,需要使用到ROOT权限。

思路
(1)HOOK 目标程序 的组件,使得我们点击组件后,能够广播信息到 测试程序,并保存起来。
(2)同时,为 目标程序 添加 接收广播类
(3)回放的时候,我们将保存的信息 广播目标程序,由 接收广播类,“回放” 动作。

为什么要使用广播?
答:广播是跨程序通信方式之一。当然其他方式也可以,只是广播比较简单而已。
Xposed模块可以传递变量吗?
答:Xposed模块在hook时(package加载时),存在于不同的进程中。即使 定义static属性,也无法跨进程传递。
使用createPackageContext获得 目标程序 Context,再获得View不行吗?
答:确实不行,获得Context不是Activity,无法获得View。(目前没有找到方法)

还有,Xposed在某个类中,无法“添加”一个属性。只能获取、修改 已定义的属性。

编写目标程序

简单的需求分析:(后面慢慢详解)
我们需要两个程序。
(1)目标程序(被测程序)。我称为target app
(2)测试程序。我称为hook app

为什么要编写 目标程序?
因为,我们需要了解 被测程序 的源码。否则,后面的hook比较麻烦。

目标程序比较简单,主要是两个文件。

activity_main.xml

主要给组件添加了点击涟漪效果。为了后面的测试点击效果 更加清晰。(其实没有清晰多少)

如何添加安卓原生点击效果?

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="100sp"
            android:text="Test1"
            android:textSize="50sp"
    />
    <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="100sp"
            android:orientation="horizontal"
            android:layout_gravity="center_horizontal">
        <TextView android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:text="请输入:"
                  android:textSize="30sp"/>
        <EditText android:id="@+id/edit11"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:hint="内容"
                  android:textSize="30sp"
                  android:width="235sp"
                  android:inputType="text"
                  android:background="?android:attr/selectableItemBackground"/>
    </LinearLayout>

    <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20sp"
            android:orientation="horizontal"
            android:layout_gravity="center_horizontal">
        <Button android:id="@+id/button11"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10sp"
                android:text="确认"
                android:textSize="40sp"
                android:foreground="?android:attr/selectableItemBackgroundBorderless"/>
        <Button android:id="@+id/button12"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="50sp"
                android:text="清空"
                android:textSize="40sp"
                android:foreground="?android:attr/selectableItemBackgroundBorderless"/>
    </LinearLayout>
    <RadioGroup
            android:id="@+id/radiogroup11"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:orientation="horizontal"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="20sp">

        <RadioButton
                android:id="@+id/radiobutton11"
                android:layout_height="wrap_content"
                android:layout_width="match_parent"
                android:textSize="30sp"
                android:text="" />

        <RadioButton
                android:id="@+id/radiobutton12"
                android:layout_height="wrap_content"
                android:layout_width="match_parent"
                android:textSize="30sp"
                android:text=""
                android:layout_marginLeft="50sp"/>

    </RadioGroup>
    <TextView
            android:id="@+id/text11"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:layout_marginTop="30sp"
            android:layout_marginLeft="10sp"
            android:hint="显示内容"
            android:textSize="30sp"
            android:textColor="#B10909"
            android:layout_marginStart="10sp"
            android:background="?android:attr/selectableItemBackgroundBorderless"/>
</LinearLayout>

MainActivity.java

package com.example.myapplication;

import android.view.View;
import android.widget.*;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    
    
    EditText et11;
    TextView tv11;
    Button bt11;
    Button bt12;

    private RadioGroup rg11;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 通过id获取对应组件
        et11 = (EditText) this.findViewById(R.id.edit11);
        tv11 = (TextView) this.findViewById(R.id.text11);

        bt11 = (Button) this.findViewById(R.id.button11);
        bt12 = (Button) this.findViewById(R.id.button12);

        rg11 = (RadioGroup) this.findViewById(R.id.radiogroup11);

        // 绑定事件
        bt11.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                String s1 = et11.getText().toString();
                String text = "1输入的内容为:"+s1;
                tv11.setText(text);
            }
        });
        bt12.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                et11.setText("");
                tv11.setText("");
                Toast.makeText(MainActivity.this,"1清空",Toast.LENGTH_SHORT).show();
            }
        });
        rg11.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
    
    
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
    
    
                RadioButton rb = (RadioButton) findViewById(checkedId);
                Toast.makeText(getApplicationContext(), rb.getText(),Toast.LENGTH_SHORT).show();
            }
        });

    }
}

功能描述:
(1)【输入框】输入内容
(2)点击【确定】后,显示在【显示内容】处。
(3)点击【清空】后,清空【输入框】、【显示内容】。
(4)【单选框】暂时无效果(只可以点击)。

【单选框】怎么写,就自己查一下吧。

展示如下:
在这里插入图片描述
在这里插入图片描述

测试程序编写

主要代码量 和 问题都在这个程序上。

测试程序界面代码

MainAcitivity主要就是3个button。文章之后 还会添加新的逻辑。

// com.example.hookreplay.MainActivity 
public class MainActivity extends AppCompatActivity {
    
    
    Button bt1,bt2,bt3;
    TextView tv1;
    static String TARGET_APP_NAME = "com.example.myapplication";
    static String HOOP_APP_NAME = "com.example.hookreplay";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取对象
        bt1 = (Button) this.findViewById(R.id.button1);
        bt2 = (Button) this.findViewById(R.id.button2);
        bt3 = (Button) this.findViewById(R.id.button3);
        tv1 = (TextView) this.findViewById(R.id.tv1);

        // 注册事件
        bt1.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                tv1.setText("开始录制");
                jumpApp();
            }
        });

        bt2.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                tv1.setText("清空录制");
            }
        });

        bt3.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                tv1.setText("回放");

                jumpApp();
                performScript();
            }
        });
    }
}

activity_main.xml主要就是3个button。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:gravity="center"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginTop="24dp"
            android:layout_marginLeft="24dp"
            android:layout_marginStart="24dp">

        <Button
                android:id="@+id/button1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="开始录制"
                android:textSize="20sp"
        />
        <Button
                android:id="@+id/button2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="清空录制"
                android:textSize="20sp"/>
        <Button
                android:id="@+id/button3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="回放"
                android:textSize="20sp"/>
    </LinearLayout>
    <TextView
            android:id="@+id/tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            android:textSize="30sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

功能描述:
(1)【开始录制】跳转到目标程序,开始录制操作。
(2)【清空录制】清空 录制内容。
(3)【回放】跳转到 目标程序,回放,清空录制内容。

在这里插入图片描述
题目要求 “不能记录点击坐标”。所以我们采用HOOK的方法 来实现。

我们需要使用到HOOK框架,Xposed。下面是一大堆参考资料:

Xposed学习

参考资料:
**github: XposedInstaller
**XposedInstaller — 安装Xposed
百度百科-钩子函数
什么是钩子函数
Android——Hook(钩子函数)动态注入代码
*Android Hook 技术
Android 集成Xposed框架
*Android插件化原理解析——概要
手把手教你Hook Android 点击事件
**抱歉,Xposed真的可以为所欲为——1.基础知识储备(上)
*抱歉,Xposed真的可以为所欲为——1.基础知识储备(下)
****Xposed的框架的使用 -----重点看这个
*初探Xposed,初学Xposed框架上手必看
*安卓Xposed简单实现

夜神模拟器下载安装

OK,因为Xposed
(1)需要ROOT权限,并且
(2)需要安装在基于ARM架构的CPU上,
所以我们需要下载其他的安卓模拟器

(1)IDEA自带的AVD,可以有基于x86的,也有基于ARM的。但是我试了下,基于ARM的运行太慢了。IDEA说,x86的比ARM的快10x
但是我等了好久,始终没有启动成功基于ARM的AVD。
(2)Genymotion,很快,但是只支持x86。如需运行ARM程序,需要添加第三方插件—— github: Genymotion_ARM_Translation
但是,我试了很久,看上去Genymotion_ARM_Translation是安装成功了。但是ARM程序仍然无法安装。


(1)下载夜神模拟器
(2)使用Xposed installer安装Xposed
在这里插入图片描述
(3)下载后,直接拖入 夜神模拟器,即开始安装。

测试程序Hook思路

有了Xposed,我们能怎么hook?我们能干嘛?
(1)AOP的思想。对一个方法 进行 “增强”。
(2)反射的思想。反射调用 类的方法,属性。


我们去hook目标程序的组件的事件,
(1)记录点击事件,
(2)将该 信息 发送(广播) 给 测试程序


编写HookTest类,实现IXposedHookLoadPackage接口,重写handleLoadPackage方法。

我们使用XposedHelpers.findAndHookMethodHook,被测程序的onCreate方法。通过反射获得所有组件。

有人可能会问:你这个所有组件都放在 被测程序的 MainActivity 属性 中,如果 组件没有在MainActivity的属性中呢?
答:简单起见。。。如果不在的话,也可以通过其他方式获取,就更复杂了。

// com.example.hookreplay.HookTest.handleLoadPackage
// XposedHelpers.findAndHookMethod
XposedHelpers.findAndHookMethod(
TARGET_APP_NAME + ".MainActivity",
lpparam.classLoader, 
"onCreate", 
Bundle.class,
new XC_MethodHook() {
    
    
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
    
    
        Class<?> c = lpparam.classLoader.loadClass(TARGET_APP_NAME + ".MainActivity");
        
        Field field1 = c.getDeclaredField("et11");
        ...其他组件
        field1.setAccessible(true);
        ...其他组件
        EditText et11 = (EditText) field1.get(param.thisObject);
        ...其他组件
        ...其他代码
});

接下来hook修改组件的 事件。

hook EditText

对于EditText。我们需要监听文本的变化。
查看源码可知TextChangedListener是可以添加多个,这里我们直接添加即可。

et11.addTextChangedListener(new TextWatcher() {
    
    
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    
    }
    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    
    }
    @Override
    public void afterTextChanged(Editable s) {
    
    
        String ss = s.toString();
        XposedBridge.log("id: "+ et11.getId() + " text after change...");
        // action log
        makeActionLog(et11, ss);
    }
});

makeActionLog(et11, ss) 保存 点击动作,并广播动作给 测试程序。后面还会提及。

hook click listener

对于需要响应 点击 的事件,
这里分两种情况,
(1)对于绑定了onClickListener的组件,我们反射获得其onClickListener对象,并添加新的功能 后 放回。

见:手把手教你Hook Android 点击事件

(2)对于未绑定onClickListener的组件,我们直接添加新的onClickListener即可。

使用view.hasOnClickListeners判断是否存在onClickListener

// et11, tv11, bt11, bt12
ArrayList<View> onClickList = new ArrayList<>(Arrays.asList(et11, tv11, bt11, bt12));
for(View v: onClickList){
    
    
    if(v.hasOnClickListeners()){
    
    
        Method method = View.class.getDeclaredMethod("getListenerInfo");
        method.setAccessible(true);
        Object mListenerInfo = method.invoke(v);

        Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
        Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
        final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);
        View.OnClickListener proxyOnClickListener = v1 -> {
    
    
            XposedBridge.log("id1: "+ v1.getId());
            onClickListenerInstance.onClick(v1);

            // action log
            makeActionLog(v1);
        };
        // 设置代理类
        field.set(mListenerInfo, proxyOnClickListener);
    }else{
    
    
        //如果没有click listener,添加 onClickListeners
        v.setOnClickListener(v1->{
    
    
            XposedBridge.log("id2: " + v1.getId());

            // action log
            makeActionLog(v1);
        });
    }
}

java的lambda表达式别忘了。

() -> {
    
    
}

最近写Scala有点多,感觉Java的
(1)分号 有点多余。(不过,也减轻了 编译器压力?)
(2)代码 有点冗长。(在其他语言,明明可以写得很简单,比如说 这个lambda表达式)

hook radioGroup

对于【单选框】,原理也是一样的,查看其源码后,可以发现比onClickListener简单。
这里hook它的OnCheckedChangeListener。其中参数checkedId就是【单选框】button的ID。

// rg11
Class<?> radioGroupClz = Class.forName("android.widget.RadioGroup");
Field field = radioGroupClz.getDeclaredField("mOnCheckedChangeListener");
field.setAccessible(true);
RadioGroup.OnCheckedChangeListener onCheckedChangeListenerInstance = (RadioGroup.OnCheckedChangeListener) field.get(rg11);
RadioGroup.OnCheckedChangeListener proxyOnCheckedChangeListener = (group, checkedId) -> {
    
    
    XposedBridge.log("id3: " + checkedId);
    onCheckedChangeListenerInstance.onCheckedChanged(group, checkedId);

    // action log
    View v = ((Activity)param.thisObject).findViewById(checkedId);
    makeActionLog(v);
};
field.set(rg11, proxyOnCheckedChangeListener);

最后,别忘了把 代理类 放回。
field.set(rg11, proxyOnCheckedChangeListener);


广播

到目前为止,我们把组件 的 事件 都hook了,添加上了自己的逻辑。
自己的逻辑就是,将 组件的点击事件,发送给 测试程序

我们定义一个 action 为一个字符串,使用空格分割,分别为

组件ID 组件名称 点击时间 [文本]

目前,[文本]仅在组件为EditText中存在。

为什么需要【点击时间】?
因为我们需要 模拟触摸事件。使用view.performClick是不会触发 点击动画的。所以需要模拟MotionEvent
参考:
android MotionEvent.obtain模拟事件,自动触发
Android模拟点击的四种方式 ---------第一种。


public void makeActionLog(View v){
    
    
makeActionLog(v,"");
}
public void makeActionLog(View v, String extra){
    
    
    long curTime = System.currentTimeMillis();
    String action = "";
    if(v instanceof EditText){
    
    
        action = v.getId() + " " +  EDIT_TEXT + " " +  Long.toString(curTime) + " " +  extra;
    }else if(v instanceof Button){
    
    
        action = v.getId() + " " + BUTTON + " " + Long.toString(curTime);
    }else if(v instanceof TextView){
    
    
        action = v.getId() + " " + TEXT_VIEW + " " + Long.toString(curTime);
    }
    XposedBridge.log("make log...: " + action);

    // 发送广播
    Intent intent = new Intent(TARGET_APP_NAME);
    intent.putExtra("action", action);
    v.getContext().sendBroadcast(intent);
}

下面讲一下广播
广播 是 安卓 跨程序通信 的一种方式。

参考:
Android跨进程通信广播(Broadcast)
Android 进程间通信IPC(二)—— Broadcast广播

广播是一种被动跨进程通讯的方式。当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据。

在应用程序中发送广播比较简单。只需要调用sendBroadcast方法即可。该方法需要一个Intent对象。通过Intent对象可以发送需要广播的数据。

接收广播 需要继承BroadcastReceiver类,并重写onReceive方法。
该接收广播类,还需要在Context中注册。

注册又分为,静态注册动态注册


这里,我们需要把接收的action保存起来。所以,选择动态注册的方式。

Save data in Broadcast receiver

我们新建一个类MyBroadcastReceiver。将得到的action保存在,静态的actionList中。

实现 单例 只是 为了方便 操作。。不是必须的。

public class MyBroadcastReceiver extends BroadcastReceiver {
    
    
    public static final String SENDER_NAME = "com.example.myapplication";
    public static ArrayList<String> actionList = new ArrayList<>();
	// 单例
    private static MyBroadcastReceiver myBroadcastReceiver = null;
    public static MyBroadcastReceiver getInstance(){
    
    
        if(myBroadcastReceiver == null){
    
    
            myBroadcastReceiver = new MyBroadcastReceiver();
        }
        return myBroadcastReceiver;
    }
    @Override
    public void onReceive(Context context, Intent intent) {
    
    
        if(!SENDER_NAME.equals(intent.getAction())){
    
    
            return;
        }
        Bundle bundle = intent.getExtras();
        if(bundle == null){
    
    
            return;
        }
        String action = bundle.getString("action");
        System.out.println("MyReceiver: " + action);
        actionList.add(action);
    }
}

之后,在测试程序的onCreate方法 注册 该 接收广播类。

// com.example.hookreplay.MainActivity
// 动态注册广播接收器
BroadcastReceiver myReceiver = MyBroadcastReceiver.getInstance();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(MyBroadcastReceiver.SENDER_NAME);
registerReceiver(myReceiver, intentFilter);

注意,onCreate中只会注册一次。在其他方法,如onResume,可能多次注册,广播将接收多次。


到目前为止,
我们 点击 目标程序 的组件之后,会向 测试程序 发送广播。
测试程序接收广播,并存储起来。


应用跳转

功能需求:在我们点击【开始录制】后,我们需要跳转到 目标程序

How to switch between Android apps programmatically
**Android 必知必会 - 使用 Intent 打开第三方应用及验证可用性

使用startActivity方法。
(1)使用ComponentName指定,跳转程序的入口。
(2)Intent.FLAG_ACTIVITY_NEW_TASK,指明该Flag之后,会新启一个“页面”。

// com.example.hookreplay.MainActivity
public void jumpApp(){
    
    
    String packageName = HookTest.TARGET_APP_NAME;
    String activityName = HookTest.TARGET_APP_NAME +".MainActivity";
    Intent intent = new Intent();
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    ComponentName componentName = new ComponentName(packageName, activityName);
    intent.setComponent(componentName);
    startActivity(intent);
}

回放功能的实现

我们将MyBroadcastReceiver接收广播类中,保存的actionList广播给 目标程序,又目标程序执行 记录的动作。

// com.example.hookreplay.MainActivity
public void performScript(){
    
    
    // 广播 在该app中存储的动作列表
    Intent intent = new Intent(HOOP_APP_NAME);
    intent.putExtra("actionArray", MyBroadcastReceiver.actionList.toArray(new String[0]));
    this.sendBroadcast(intent);
}

有同学可能会问:接收广播类 不是需要 注册 吗?
是的,所以我们需要 在 目标程序 中,动态注册 一个 广播接收类。

接着之前的代码,hook目标程序onCreate方法。

我们解析action,并按照点击的时间进行延迟执行。使用handler.postDelayed

long curTime = Long.parseLong(e[2]) + ACTION_DELAY;  // 所有动作延后ACTION_DELAY

设第一个action的时间为基准(为0),之后的动作的时间都减去第一的动作的时间。同时,我们将所有动作延迟ACTION_DELAY不延迟也行

// com.example.hookreplay.HookTest
// handleLoadPackage.XposedHelpers.findAndHookMethod
// add a broadcast receiver to target app
class TargetReceiver extends BroadcastReceiver{
    
    
     public static final String SENDER_NAME = "com.example.hookreplay";
     public static final long ACTION_DELAY = 1000;
     private void performTouch(View v){
    
    
         long downTime = SystemClock.uptimeMillis();
         MotionEvent downEvent = MotionEvent.obtain(downTime, downTime+200, MotionEvent.ACTION_DOWN, 0, 0 , 0);
         downTime += 600;
         MotionEvent upEvent = MotionEvent.obtain(downTime, downTime+100, MotionEvent.ACTION_UP, 0, 0 , 0);
         v.onTouchEvent(downEvent);
         v.onTouchEvent(upEvent);
         upEvent.recycle();
         downEvent.recycle();
     }
     @Override
     public void onReceive(Context context, Intent intent) {
    
    
         if(!SENDER_NAME.equals(intent.getAction())){
    
    
             return;
         }
         Bundle bundle = intent.getExtras();
         if(bundle == null){
    
    
             return;
         }
         String[] actionArray = bundle.getStringArray("actionArray");

         if(actionArray.length == 0){
    
    
             Toast.makeText((Activity)param.thisObject,"请先录制",Toast.LENGTH_SHORT).show();
             return;
         }
         // 遍历action列表,每隔ACTION_DELAY执行一次动作
         Toast.makeText((Activity)param.thisObject,"开始播放",Toast.LENGTH_SHORT).show();
         final Handler handler = new Handler(Looper.getMainLooper());
         long startTime = Long.parseLong(actionArray[0].split(" ")[2]);
         long endTime = Long.parseLong(actionArray[actionArray.length-1].split(" ")[2]);
         for(String action: actionArray) {
    
    
             System.out.println("perform script:  "+ action);
             String[] e = action.split(" ");
             if (e.length == 0) {
    
    
                 continue;
             }
             int id = Integer.parseInt(e[0]);
             String compName = e[1];
             long curTime = Long.parseLong(e[2]) + ACTION_DELAY;  // 所有动作延后ACTION_DELAY

             View v = ((Activity)context).findViewById(id);
             handler.postDelayed(() -> {
    
    
                 System.out.println("execute action: " + action);
                 switch (compName) {
    
    
                     case HookTest.BUTTON:
                         performTouch(v);
                         break;
                     case HookTest.TEXT_VIEW:
                         performTouch(v);
                         break;
                     case HookTest.EDIT_TEXT:
                         if(e.length >= 4){
    
    
                             String text = e[3];
                             ((EditText) v).setText(text);
                         }else{
    
    
                             performTouch(v);
                         }
                         break;
                     default:
                         break;
                 }
             }, (curTime - startTime));
         }

         handler.postDelayed(()->{
    
    
             Toast.makeText((Activity)param.thisObject,"回放结束",Toast.LENGTH_SHORT).show();
         }, (endTime-startTime) + ACTION_DELAY*2);
     }
 };

为什么不用view.performClick
参考:
android MotionEvent.obtain模拟事件,自动触发
Android模拟点击的四种方式 ---------第一种。

因为我们需要 模拟触摸事件。使用view.performClick是不会触发 点击动画的。所以需要模拟MotionEvent
*
需要注意的是,MotionEvent的时间需要是SystemClock.uptimeMillis()中获得的。

如何实现【延迟】执行代码?
How to delay a loop in android without using thread.sleep?
[solve] How to delay a loop in android without using thread.sleep?

最后,别忘记注册了。

// 注册receiver
BroadcastReceiver bcr = new TargetReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TargetReceiver.SENDER_NAME);
((Activity)param.thisObject).registerReceiver(bcr,intentFilter);

OK,至此,我们已经完成所有逻辑的编写。

下面是这几天的 学习踩坑。仅作为记录,思路不完成正确。


2022-05-(12-13)

HOOK是什么?

参考:
百度百科
什么是钩子函数

钩子函数是Windows消息处理机制的一部分,通过设置“钩子”,应用程序可以在系统级对所有消息、事件进行过滤,访问在正常情况下无法访问的消息。钩子的本质是一段用以处理系统消息的程序,通过系统调用,把它挂入系统。


钩子函数:钩子函数是在一个事件触发的时候,在系统级捕获到了他,然后做一些操作。一段用以处理系统消息的程序。“钩子”就是在某个阶段给你一个做某些处理的机会。

钩子函数: 1、是个函数,在系统消息触发时被系统调用 2、不是用户自己触发的

钩子函数的名称是确定的,当系统消息触发,自动会调用。例如react的componentWillUpdate函数,用户只需要编写componentWillUpdate的函数体,当组件状态改变要更新时,系统就会调用componentWillUpdate。


简单地说,钩子函数是处理系统消息的机制。
但是上面说的都是桌面window的钩子函数吧,有没有安卓的?

一段简单的HOOK的例子

Android——Hook(钩子函数)动态注入代码

总结一下,HOOK对关键组件进行代理、替换,并增加新的功能,实现我们的目的。
HOOK用到的技术思想:AOP、代理、反射。

IDEA安卓开发

如何用idea进行安卓开发

遇到的问题:
(1)bulid工程失败?
build.gradle添加

dependencies {
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation "androidx.test.ext:junit:1.1.3"
    androidTestImplementation "androidx.test:core:1.4.0"
    androidTestImplementation "androidx.fragment:fragment-testing:1.4.1"
}

同时修改android.compileSdk=31android.defaultConfig.targetSdk=31

目标程序编写

因为功能简单(其实是不懂)所以就简单的写一个程序。

参考:
通过实例学Android应用开发01 - 根据此改写。
Android应用程序结构及运行原理


安卓模拟器genymotion

各位应该注意到了,idea自带的AVD模拟器很卡。为了进一步增加开发体验,我决定下一个安卓模拟器genymotion。

参考:
Genymotion配置及使用教程(最新最完整版附各部分下载地址)
Android模拟器Genymotion使用详解
genymotion官网

genymotion官网下载,注意选择下载with virtual box

genymotion下载完成后,点击右上角的+,下载安装安卓镜像。
在这里插入图片描述

idea打包apk

将之前写好的目标程序打包成apk,拖入模拟器中。

IntelliJ IDEA 如何导出安卓(Android)apk文件 详细教程

将得到的apk文件,直接拖入genymotion模拟器中即可自动安装。
在这里插入图片描述
向上滑,打开应用程序目录。

在这里插入图片描述

不得不说,卡顿感明显少了许多。接下来是回放程序的制作。应该就是难点了。

genymotion不支持ARM?

解决Genymotion不兼容arm框架的问题

拖拽Genymotion-ARM-Translation.zip无法直接安装?

github: Genymotion_ARM_Translation

找到adb工具,genymotion应该自带,比如说我的位置在E:\genymotion\tools

  1. adb shell
  2. cd /sdcard/Download/
  3. sh /system/bin/flash-archive.sh /sdcard/Download/Genymotion-ARM-Translation.zip
  4. adb reboot

执行后,会出现错误。一看,上传上去的文件名字不对。
自己上去修改。修改名字之后,再进行上面的命令。

cd /sdcard/Download/
mv ??? Genymotion-ARM-Translation.zip

2022-05-14

之前提到了HOOK。为什么要用到它,从功能是实现上来讲,我们需要做的是两部分功能。
(1)使用HOOK监听用户操作,并将操作记录 存储起来。
(2)读取操作记录,实现用户操作。
(3)HOOK程序,和 目标程序应该是两个程序。

操作记录的存储 可以 使用文件,不过这样会更复杂一点。
(1)大概了解了,是使用HOOK实现。
(2)到底要怎么实现呢?
(3)两个程序 是需要考虑进程之间的通信?

HOOK框架?

*Android Hook 技术
Android 集成Xposed框架

好像目前HOOK框架,比如说Xposed这种,都需要ROOT权限,有没有更简单的方法???

VirtualXposed
epic 可以实现任意方法 Hook,但只能对同一个 uid 的进程生效,VA 可以运行任意 APP,使其与自己使用同一个 uid。结合上面两种技术,有没有想到什么?
是的,这就是 VirtualXposed,结合了 epic 与 VA 的终极框架。无需 root,类 Xposed 的 API,可以说是一个相当完善的方案了。

2022-05-14 20:50

成功安装了Genymotion-ARM-Translation,但是VirtualXposed.apk还是无法安装,貌似还是一样的错误。(还以为很快解决这个问题的,太折磨了)

2022-05-14 21:20

忘了好多博客提醒的,路径不能含有中文!这样的话,Genymotion-ARM-Translation.zip就可以直接拖拽安装了。

2022-05-15 10:20

失败,Genymotion还是用不上ARM应用,也不知道是不是版本的问题。但是Genymotion官网上已经没有其他的版本了。

android hook截取其他程序的按钮事件_android程序员hook技术之入门篇

2022-05-15

*Android插件化原理解析——概要

既然HOOK框架不行的话,或许不用框架也可以实现需要的功能?


2022-05-15 18:55
手把手教你Hook Android 点击事件
hook onClickListener

2022-05-15 23:43
下班,如果是只实现一个程序的话,大概知道要怎么做了。两个程序的话,有点麻烦。(明天再写吧
2022-05-17 20:52
需要两个应用,该思路忘记掉。

2022-05-16

夜神模拟器 & Xposed

(1)下载夜神模拟器
(2)使用Xposed installer 安装Xposed

github: XposedInstaller
XposedInstaller
*初探Xposed,初学Xposed框架上手必看
*安卓Xposed简单实现
2022-05-16 19:46
编写第一个Xposed程序。

2022-05-17

*抱歉,Xposed真的可以为所欲为——1.基础知识储备(上)
*抱歉,Xposed真的可以为所欲为——1.基础知识储备(下)

如何切换应用?

How to switch between Android apps programmatically
*Android 必知必会 - 使用 Intent 打开第三方应用及验证可用性

2022-05-18

跨进程通信之——广播

Android跨进程通信广播(Broadcast)
Android 进程间通信IPC(二)—— Broadcast广播

A应用如何在B应用上执行点击操作???

Xposed跨进程通信?

基于xposed实现android注册系统服务,解决跨进程共享数据问题
太复杂了。。。

获取其他应用Context

*Android获取其他包的Context实例,然后调用它的方法

广播数据如何保存?

Save data in Broadcast receiver

使用动态注册的广播对象,会一直保存数据。

PackageManager.NameNotFoundException

solved: PackageManager.NameNotFoundException

原因:api30之后安全策略的变化。
解决:添加配置文件。

如何获得其他应用的Context?

应用A,应用B
在A上 控制B的 UI。
=》在A中获得B的Context
或者 Hook B之后,在B中把Context实例发给A。

2022-05-20 该方法不行。

2022-05-19

2022-05-19 10:36

***Xposed的框架的使用

Activity生命周期

Activity生命周期

Service

Android Service详解
可能会用到。

Array[String] to String[]

Converting 'ArrayList to ‘String[]’ in Java

.toArray(new String[0])

如何获取当前系统时间?

Java如何获取当前日期和时间?

如何模拟触摸事件?

android MotionEvent.obtain模拟事件,自动触发
Android模拟点击的四种方式 ---------第一种。

使用view.performClick是不会触发 点击动画的。所以需要模拟MotionEvent

猜你喜欢

转载自blog.csdn.net/LittleSeedling/article/details/124769084
今日推荐