2231 words
11 minutes
无障碍开发2-后台搭建

本节基于Django来开发API和管理平台,从而让管理员可以实现在页面上进行控制手机的执行。

image-20240703160514985

1.django开发#

1.1 环境和启动#

image-20240705210555545

pip install django
  • 创建项目

    django-admin startproject mysite
    
  • 创建app

    cd mysite
    python manage.py startapp app01
    
  • 启动程序

    python manage.py runserver 127.0.0.1:8001
    

1.2 ORM#

from django.db import models


class Device(models.Model):
    """ 设备表 """
    device_id = models.CharField(verbose_name="设备ID", max_length=32, db_index=True)
    dy_account = models.CharField(verbose_name="抖音账号", max_length=64)
    model = models.CharField(verbose_name="手机型号", max_length=64)

    class Meta:
        verbose_name_plural = "设备表"

    def __str__(self):
        return f"{self.device_id} - {self.dy_account}"


class Task(models.Model):
    """ 任务表 """
    device = models.ForeignKey(verbose_name="设备账号", to="Device", on_delete=models.CASCADE)
    address = models.TextField(verbose_name="直播分享地址")
    text = models.TextField(verbose_name="评论内容", help_text="回车换行,手机会按照顺序进行评论")
    counter = models.IntegerField(verbose_name="循环次数", default=1)
    success_count = models.IntegerField(verbose_name="已评论次数", default=0)
    status = models.SmallIntegerField(verbose_name="状态", choices=((0, "待执行"), (1, "执行中"), (2, "已完成")), default=0)

    class Meta:
        verbose_name_plural = "任务"

1.3 接口开发#

from django.contrib import admin
from django.urls import path
from api import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('fetch/task/<str:device_id>/', views.fetch_task),
    path('running/task/<str:task_id>/', views.running_task),
    path('complete/task/<str:task_id>/', views.complete_task),
]
from django.http import JsonResponse
from django.db.models import F
from api import models


def fetch_task(request, device_id):
    instance = models.Task.objects.filter(device__device_id=device_id, status=0).order_by("id").first()
    if not instance:
        return JsonResponse({"status": False})

    context = {
        "status": True,
        "address": instance.address,
        "textList": instance.text.split("\n"),
        "counter": instance.counter,
        "taskId": instance.id
    }
    instance.success_count = 0
    instance.status = 1
    instance.save()
    return JsonResponse(context)


def running_task(request, task_id):
    models.Task.objects.filter(id=task_id, status=1).update(success_count=F("success_count") + 1)
    return JsonResponse({"status": True})


def complete_task(request, task_id):
    models.Task.objects.filter(id=task_id, status=1).update(status=2)
    return JsonResponse({"status": True})

1.4 Admin#

django默认提供了一个后台管理,用于对数据库中的表进行快速的增删改查。

import re

from django.contrib import admin
from django.utils.safestring import mark_safe
from api import models
from django import forms


class DeviceAdmin(admin.ModelAdmin):
    list_display = ('device_id', 'dy_account', "model")


admin.site.register(models.Device, DeviceAdmin)


class TaskAdmin(admin.ModelAdmin):
    class TaskModelForm(forms.ModelForm):
        class Meta:
            model = models.Task
            # fields = "__all__"
            exclude = ["success_count"]
            widgets = {
                "address": forms.Textarea(attrs={"rows": 4, "cols": 40,"class":"vLargeTextField"})
            }

    form = TaskModelForm

    def address_display(self, obj):
        return re.findall(".*?【(.+?)】.*", obj.address)

    address_display.__name__ = "直播间名称"

    def total_count(self, obj):
        return len(obj.text.split("\n")) * obj.counter

    total_count.__name__ = "任务数量"

    def display_text(self, obj):
        return mark_safe(f"<pre>{obj.text}</pre>")

    display_text.__name__ = "评论"
    list_display = (
        "id", 'device', 'address_display', "display_text", "counter",
        'total_count', "success_count", "status"
    )
    list_display_links = ["device"]
    class DeviceFilter(admin.SimpleListFilter):
        title = '设备'
        parameter_name = 'device'

        def lookups(self, request, model_admin):
            return models.Device.objects.values_list('id', 'device_id')

        def queryset(self, request, queryset):
            device_value = self.value()
            if not device_value:
                return queryset
            return queryset.filter(device=self.value())

    list_filter = ["status", DeviceFilter]


admin.site.register(models.Task, TaskAdmin)

创建admin账号:

python manage.py createsuperuser

image-20240703161930407

2.无障碍应用#

2.1 网络配置#

image-20240703162419902

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
android:networkSecurityConfig="@xml/network_security_config"
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!--禁用掉明文流量请求的检查-->
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
    implementation "com.squareup.okhttp3:okhttp:4.9.1"
    implementation 'com.google.code.gson:gson:2.8.6'

注意:如果app还是无法发送请求,就卸载app重新安装。

2.2 设备和服务器#

image-20240703163026174

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- IP地址 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="地址:" />

            <EditText
                android:id="@+id/txt_address"
                android:layout_width="180dp"
                android:layout_height="match_parent"
                android:singleLine="true"
                android:textSize="14dp" />

        </LinearLayout>

        <!-- 设备 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="设备名称:" />

            <EditText
                android:id="@+id/txt_device"
                android:layout_width="180dp"
                android:layout_height="match_parent"
                android:singleLine="true"
                android:textSize="14dp" />

        </LinearLayout>

        <!-- 点击启动 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center">

            <Button
                android:id="@+id/btn_start"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="5dp"
                android:text="启动" />
        </LinearLayout>

        <!-- 停止服务 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center">

            <Button
                android:id="@+id/btn_stop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="5dp"
                android:text="终止无障碍服务" />
        </LinearLayout>

    </LinearLayout>

</LinearLayout>
package com.nb.autodemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private Button btnStart;
    private Button btnStop;
    private TextView txtDevice;
    private TextView txtAddress;

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

        initView();
        initListener();
        // 引导用户开启无障碍
        checkAccessibility();
    }


    private void initView() {
        btnStart = findViewById(R.id.btn_start);

        txtDevice = findViewById(R.id.txt_device);
        String android_id = Settings.System.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID);
        Log.e("设备ID",android_id);
        txtDevice.setText(android_id);

        txtAddress = findViewById(R.id.txt_address);
        txtAddress.setText(new String("127.0.0.1:8000"));

    }

    private void initListener() {
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                start();
            }
        });

        btnStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
            }
        });
    }
    private void start() {
        // LuckyAccessibilityService.allowStart = true;
        LuckyAccessibilityService.startTask(
                String.valueOf(txtDevice.getText()),
                String.valueOf(txtAddress.getText())
        );
        
    }

    /**
     * 引导开启无障碍模式
     */
    private void checkAccessibility() {
        if (!isAccessibilitySettingsOn(getApplicationContext())) {
            Toast.makeText(this, "请先打开无障碍服务", Toast.LENGTH_LONG).show();
            startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
        }
    }

    private boolean isAccessibilitySettingsOn(Context mContext) {
        int accessibilityEnabled = 0;
        final String service = getPackageName() + "/" + LuckyAccessibilityService.class.getCanonicalName();
        try {
            accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(), android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);

        } catch (Settings.SettingNotFoundException e) {
            Log.e("主界面", "配置文件没找到: " + e.getMessage());
        }

        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
        if (accessibilityEnabled == 1) {
            String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                mStringColonSplitter.setString(settingValue);
                while (mStringColonSplitter.hasNext()) {
                    String accessibilityService = mStringColonSplitter.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
public class LuckyAccessibilityService extends AccessibilityService {

    public static String deviceName;
    public static String ipAddress;
    public static boolean allowStart = false;
    
	public static void startTask(String device, String address) {
        deviceName = device;
        ipAddress = address;
        allowStart = true;
    }
}

2.3 示例代码#

package com.nb.autolive;


import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.os.Bundle;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.graphics.Rect;

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;

import com.google.gson.Gson;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

class NetTaskResponse {
    public boolean status;
    public String address;
    public ArrayList<String> textList;
    public int counter;
    public int taskId;
}

public class LuckyAccessibilityService extends AccessibilityService {

    public static String deviceName;
    public static String ipAddress;
    public static boolean allowStart = false;
    public static boolean running = false;

    public static Thread runThread = null;

    @Override
    public void onInterrupt() {

    }


    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 手机内部自动触发调用,手机有一些变动,自动执行(系统)
        // Log.e("无障碍", "来了");
        // 开启无障碍模式后,方法会自动调用(寻找相关标签、点击、欢动)
        if (!allowStart) {
            return;
        }

        if (!running) {
            running = true;
            if (runThread != null && runThread.isAlive()) {
                return;
            }
            // Log.e("启动", "创建线程");
            // 创建线程去执行抖音薅羊毛任务(只有一个线程)
            runThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 后续代码(新创建一个线程 + 防止创建很多线程)
                    // Log.e(String.valueOf(runThread.getId()), "开始");
                    loopTask();
                }
            });
            runThread.start();
        }

    }

    public static void startTask(String device, String address) {
        deviceName = device;
        ipAddress = address;
        allowStart = true;
    }

    public void loopTask() {
        while (true) {
            try {
                // 1.领取任务,有任务再执行,无任务则不执行。
                NetTaskResponse response = netFetchTask();
                if (response == null || !response.status) {
                    // Log.e(String.valueOf(runThread.getId()), "无任务");
                    Thread.sleep(10000);
                    killAllProcess();
                    continue;
                }

                // 2.执行任务
                doTask(response);
            } catch (Exception ex) {
                // Log.e("异常", ex.toString());
                ex.printStackTrace();
            } finally {
            }
        }
    }

    public NetTaskResponse netFetchTask() {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url("http://" + ipAddress + "/fetch/task/" + deviceName + "/").build();
        Call call = client.newCall(request);
        try {
            Response res = call.execute();
            ResponseBody body = res.body();
            String dataString = body.string();
            NetTaskResponse response = new Gson().fromJson(dataString, NetTaskResponse.class);
            return response;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void netRunningTask(int taskId) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url("http://" + ipAddress + "/running/task/" + taskId + "/").build();
        Call call = client.newCall(request);
        try {
            call.execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void netCompleteTask(int taskId) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url("http://" + ipAddress + "/complete/task/" + taskId + "/").build();
        Call call = client.newCall(request);
        try {
            call.execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void doTask(NetTaskResponse response) throws InterruptedException {

        // 1.回到首页 & 杀死所有其他进程(记得开启无障碍应用保护)
        Thread.sleep(randomInt(2, 5) * 1000L);
        killAllProcess();

        // 2.滑动屏幕,寻找抖音app,打开app
        Thread.sleep(randomInt(2, 5) * 1000L);
        if (!launchApplicationByName("抖音", 10)) {
             Log.e(String.valueOf(runThread.getId()), "无法启动抖音");
            return;
        }

        // 3.点击搜索框
        Thread.sleep(randomInt(5, 10) * 1000L);
        AccessibilityNodeInfo searchNode = findSearchNode();
        if (searchNode == null) {
            return;
        }
        clickByNodeClickable(searchNode);

        // 4.输入直播间分享地址
        Thread.sleep(randomInt(5, 10) * 1000L);
        AccessibilityNodeInfo searchTextNode = findNodeById("com.ss.android.ugc.aweme:id/et_search_kw");
        Bundle arguments = new Bundle();
        arguments.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, response.address);
        searchTextNode.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments);

        // 5.点击搜索,进入直播间
        Thread.sleep(randomInt(5, 10) * 1000L);
        AccessibilityNodeInfo doSearchNode = findNodeById("com.ss.android.ugc.aweme:id/zy=");
        clickByNodePosition(doSearchNode);

        Thread.sleep(randomInt(10, 20) * 1000L);
        for (int i = 0; i < response.counter; i++) {
            for (int j = 0; j < response.textList.size(); j++) {
                try {
                    // 随机获取评论内容
                    String text = response.textList.get(j);

                    // 6.寻找评论位置,根据"说点什么..."
                    Thread.sleep(randomInt(5, 10) * 1000L);
                    AccessibilityNodeInfo commentNode = findNodeByText("说点什么...");
                    if (commentNode == null) {
                        commentNode = findNodeByText("聊一聊");
                        if (commentNode == null) {
                            return;
                        }
                    }
                    clickByNodeClickable(commentNode);

                    // 7.输入评论
                    Thread.sleep(randomInt(5, 10) * 1000L);
                    AccessibilityNodeInfo sayNode = findNodeById("com.ss.android.ugc.aweme:id/f4f");
                    AccessibilityNodeInfo child = sayNode.getChild(0);
                    Bundle arguments1 = new Bundle();
                    arguments1.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
                    child.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments1);

                    // 8.发送评论
                    Thread.sleep(randomInt(2, 5) * 1000L);
                    AccessibilityNodeInfo sendNode = findNodeById("com.ss.android.ugc.aweme:id/z0c");
                    clickByNodeClickable(sendNode);

                    netRunningTask(response.taskId);

                    // 9.间隔
                    Thread.sleep(randomInt(30, 120) * 1000L);
                } catch (Exception e) {

                }
            }
        }

        netCompleteTask(response.taskId);
    }

    private AccessibilityNodeInfo findSearchNode() {
        try {
            return findNodeById("com.ss.android.ugc.aweme:id/kq3");
        } catch (Exception e) {
            Log.e("进直播间", "【异常】寻找搜索框失败");
        }
        return null;
    }

    /**
     * 杀死所有进程(记得提前锁定无障碍应用)
     */
    public void killAllProcess() throws InterruptedException {
        // 1.回到首页 Home
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);

        // 杀死其他应用
        performGlobalAction(GLOBAL_ACTION_RECENTS);
        Thread.sleep(2000);

        String[] clearAppIdArray = new String[]{
                "com.huawei.android.launcher:id/clear_all_recents_image_button", // 华为v30 pro
                "com.hihonor.android.launcher:id/clear_all_recents_image_button", // 荣耀
                "com.android.systemui:id/clearAnimView" // 小米8A
        };
        for (String id : clearAppIdArray) {
            // 根据ID找到节点
            AccessibilityNodeInfo clearIdNode = findNodeById(id);
            if (clearIdNode != null) {
                // 点击标签
                clickByNodeClickable(clearIdNode);
                break;
            }
        }
    }

    /**
     * 根据ID找元素
     */
    private AccessibilityNodeInfo findNodeById(String id) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId(id);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * 根据文本找元素
     */
    private AccessibilityNodeInfo findNodeByText(String text) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByText(text);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }


    /**
     * 点击节点 点击
     */
    private void clickByNodeClickable(AccessibilityNodeInfo node) {
        if (node.isClickable()) {
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            node.recycle();
        } else {
            AccessibilityNodeInfo parent = node.getParent();
            node.recycle();
            clickByNodeClickable(parent);
        }
    }


    /**
     * 根据坐标 点击
     */
    private void clickByNodePosition(AccessibilityNodeInfo node) {

        Rect rc = new Rect();
        node.getBoundsInScreen(rc);
        Path path = new Path();
        path.moveTo(rc.centerX(), rc.centerY());
        GestureDescription.Builder builder = new GestureDescription.Builder();
        GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200)).build();
        dispatchGesture(description, null, null);
    }

    /**
     * 在页面上寻找名字叫啥的app
     */
    public boolean launchApplicationByName(String name, int retry) throws InterruptedException {
        for (int i = 0; i < retry; i++) {
            // 翻看右边的页面
            slideScreen(540, 800, 100, 800, 500L, 200L);
            Thread.sleep(3000);

            // 找抖音APP
            AccessibilityNodeInfo app = findNodeByText(name);
            if (app != null) {
                Thread.sleep(500);
                clickByNodeClickable(app);
                Thread.sleep(5000);
                return true;
            }
        }
        return false;
    }

    /**
     * 滑动屏幕
     */
    private boolean slideScreen(int startX, int startY, int endX, int endY, Long startTime, Long duration) {
        try {
            Path path = new Path();
            path.moveTo(startX, startY);
            path.lineTo(endX, endY);
            GestureDescription.Builder builder = new GestureDescription.Builder();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 500L, 37L)).build();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 1000L, 500L)).build();
            GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, startTime, duration)).build();
            dispatchGesture(description, null, null);
            return true;
        } catch (Exception e) {
            // Log.e(TAG, "滑动屏幕失败,详细:" + e.toString());
            Log.e("无障碍", "滑动屏幕失败");
        }
        return false;
    }

    /**
     * 生成随机值
     */
    private int randomInt(int minValue, int maxValue) {
        Random rand = new Random();
        return rand.nextInt(maxValue - minValue) + minValue;
    }

}
无障碍开发2-后台搭建
https://zycreverse.netlify.app/posts/无障碍2/
Author
会写点代码的本子画手
Published at
2025-04-23