2231 words
11 minutes
无障碍开发2-后台搭建
本节基于Django来开发API和管理平台,从而让管理员可以实现在页面上进行控制手机的执行。
1.django开发
1.1 环境和启动
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
2.无障碍应用
2.1 网络配置
<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 设备和服务器
<?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/