如果频繁点击安卓屏幕的某个按钮,是不是觉得很繁琐很累?下面来写个脚本来实现自动点击。以下面的两个EditText和一个Button为例的一个demoapp,来演示如何自动输入文本和自动点击按钮:
这个界面的代码:
MainActivity.kt
package com.kwai.llcrm.del
import android.os.Bundle
import android.widget.EditText
import android.widget.Space
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kwai.llcrm.del.ui.theme.DelTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
loginContainer()
}
}
}
}
@Preview(showBackground = true)
@Composable
fun loginContainer() {
val context = LocalContext.current
var phoneNumber by remember { mutableStateOf("") }
var pwd by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(0.dp, 60.dp, 0.dp, 0.dp)
) {
// Spacer(modifier = Modifier.height(20.dp))
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp, 20.dp, 10.dp, 0.dp)
.height(65.dp),
value = phoneNumber,
onValueChange = {
phoneNumber = it
},
placeholder = {
Text(text = "phone number")
})
// Spacer(modifier = Modifier.height(20.dp))
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp, 20.dp, 10.dp, 0.dp),
value = pwd,
onValueChange = {
pwd = it
},
placeholder = {
Text(text = "pwd")
})
Button(
modifier = Modifier
.padding(20.dp, 20.dp, 0.dp, 0.dp)
.width(150.dp)
.height(55.dp),
onClick = {
Toast.makeText(context, "click login", Toast.LENGTH_SHORT).show()
}) {
Text(text = "login")
}
}
}
一:获取触摸坐标
在手机的开发者选项里打开“指针位置”单选框,如图:
这样的话, 手机的顶部会实时显示手指触摸屏幕的x、y坐标位置,这两个坐标位置是脚本里的需要的参数。
二:编写adb命令
打开demoapp,分别找到两个EditText和一个Button的共六个x、y坐标信息:username_field_x_coord
、 username_field_y_coord
、password_field_x_coord
、 password_field_y_coord
、login_btn_x_coord
and login_btn_y_coord
。
有了这六个参数,adb命令如下:
# 手机信息
adb shell input tap 250 460 && \
adb shell input text "{mobile_number}" && \
# 密码
adb shell input tap 250 700 && \
adb shell input text "{pwd}" && \
# 登陆按钮
adb shell input tap 280 900 && \
# 等待3秒,输入optcode,可选项
sleep 3 && adb shell input text "optcode"
把上面的命令写入shell脚本或者python脚本,脚本里可以自己添加一些循环条件来实现频繁点击,运行脚本即可实现自动化,这样比人工点击方便多了。
三:用python搭建一个本地简易服务器
下面我们写一个python脚本,来搭建一个简易的本地服务器,并与一个index.html前端交互来调用上面第二节介绍的adb命令。
创建一个login_server.py
,内容如下:
from flask import Flask, send_from_directory, request
import os
import logging
app = Flask(__name__)
@app.route('/')
def index():
return send_from_directory('static', 'index.html')
@app.route('/trigger-adb')
def trigger_adb():
mobile_number = request.args.get('mobileNumber') # default number if not provided
pwd = request.args.get('pwd')
logging.warning(f"start adb phone number={mobile_number} and pwd={pwd}")
print(f"start adb phone number is {mobile_number} and pwd is {pwd}")
adb_command = f'adb shell input tap 250 460 && adb shell input text "{mobile_number}" && adb shell input tap 250 700 && adb shell input text "{pwd}" && adb shell input tap 280 900 && sleep 3 && adb shell input text "optcode"'
# Execute the ADB command
os.system(adb_command)
return 'ADB command executed successfully!'
@app.route('/clear-app-data') # for loggin out of the app
def clear_app_data():
# Replace with your actual app package name and main activity
package_name = 'com.kwai.llcrm.del'
main_activity = 'com.kwai.llcrm.del.MainActivity' # launcher activity
# Clear app data
clear_command = f'adb shell pm clear {package_name}'
os.system(clear_command)
# Relaunch the app
launch_command = f'adb shell am start -n {package_name}/{main_activity}'
os.system(launch_command)
return 'App data cleared and app relaunched successfully!'
if __name__ == '__main__':
app.run(port=5000)
需要安装flask库。执行下面命令即可:
pip install flask
上面的login_server.py
里,可以看到,写了三个函数:
- 第一个函数是访问static/index.html
- 第二个函数是static/index.html调用,就是执行上面第二节介绍的adb命令,实现自动化操作。
- 第三个函数是static/index.html调用,是对一个app进行清理数据的操作。
执行命令python login_server.py
,即在本地起了端口号为5000的服务,运行地址是http://127.0.0.1:5000
。
四:创建前端页面
在login_server.py
同一目录下,创建static
目录,在static
目录下分别创建index.html
和style.css
, 内容分别如下:
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App Control Panel</title>
<link rel="stylesheet" href="static/styles.css">
</head>
<body>
<div class="container">
<h1>App Control Panel</h1>
<input type="text" id="mobileNumber" placeholder="Enter mobile number">
<input type="text" id="extPwd" placeholder="Enter password">
<div class="button-group">
<button id="login" class="btn-custom">Login</button>
<button id="logout" class="btn-custom">Logout</button>
</div>
<ul id="suggestions" class="suggestions"></ul>
<div id="message" class="message"></div>
<div class="bottom-buttons">
<button id="clearAll" class="clear-all-button">Clear Suggestions</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load recent numbers from cookies and display as suggestions
const recentNumbers = getRecentNumbers();
displaySuggestions(recentNumbers);
document.getElementById('login').addEventListener('click', function() {
const mobileNumber = document.getElementById('mobileNumber').value;
const pwd = document.getElementById('extPwd').value;
if (mobileNumber && pwd) {
performLogin(mobileNumber, pwd);
}
});
document.getElementById('logout').addEventListener('click', function() {
fetch('http://127.0.0.1:5000/clear-app-data')
.then(response => response.text())
.then(data => document.getElementById('message').innerText = data)
.catch(error => console.error('Error:', error));
});
document.getElementById('clearAll').addEventListener('click', function() {
setRecentNumbers([]);
displaySuggestions([]);
});
});
function getRecentNumbers() {
const cookies = document.cookie.split(';');
const recentNumbers = cookies.find(cookie => cookie.trim().startsWith('recentNumbers='));
return recentNumbers ? JSON.parse(decodeURIComponent(recentNumbers.split('=')[1])) : [];
}
function setRecentNumbers(numbers) {
document.cookie = `recentNumbers=${encodeURIComponent(JSON.stringify(numbers))}; path=/`;
}
function addRecentNumber(number) {
let recentNumbers = getRecentNumbers();
if (!recentNumbers.includes(number)) {
recentNumbers.push(number);
if (recentNumbers.length > 5) { // Limit to 5 recent numbers
recentNumbers.shift();
}
setRecentNumbers(recentNumbers);
displaySuggestions(recentNumbers);
}
}
function displaySuggestions(numbers) {
const suggestionsList = document.getElementById('suggestions');
suggestionsList.innerHTML = '';
numbers.forEach(number => {
const listItem = document.createElement('li');
listItem.textContent = number;
listItem.dataset.number = number; // Store number in data attribute
const removeIcon = document.createElement('span');
removeIcon.className = 'remove-icon';
removeIcon.innerHTML = '✕'; // Cross icon (✗)
removeIcon.onclick = function() {
let recentNumbers = getRecentNumbers();
recentNumbers = recentNumbers.filter(num => num !== number);
setRecentNumbers(recentNumbers);
displaySuggestions(recentNumbers);
};
listItem.appendChild(removeIcon);
listItem.addEventListener('click', function() {
document.getElementById('mobileNumber').value = number; // Update text field
const pwd = document.getElementById('extPwd').value;
performLogin(number, pwd); // Perform login when suggestion is clicked
});
suggestionsList.appendChild(listItem);
});
}
function performLogin(mobileNumber, pwd) {
addRecentNumber(mobileNumber);
fetch(`http://127.0.0.1:5000/trigger-adb?mobileNumber=${encodeURIComponent(mobileNumber)}&pwd=${encodeURIComponent(pwd)}`)
.then(response => response.text())
.then(data => document.getElementById('message').innerText = data)
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>
style.css:
body {
font-family: 'Product Sans', sans-serif;
background: linear-gradient(135deg, #1e3a8a, #4b6cb7);
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: #333;
}
.container {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 40px;
max-width: 500px;
width: 100%;
text-align: center;
position: relative;
overflow: hidden;
}
h1 {
margin-bottom: 30px;
font-size: 32px;
color: #007bff;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2);
}
input[type="text"] {
width: calc(100% - 40px);
padding: 18px;
border: 1px solid #ced4da;
border-radius: 8px;
font-size: 20px;
margin-bottom: 30px;
box-sizing: border-box;
outline: none;
transition: border-color 0.3s, box-shadow 0.3s;
}
input[type="text"]:focus {
border-color: #007bff;
box-shadow: 0 0 8px rgba(0, 123, 255, 0.5);
}
.button-group {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}
.button-group button {
background-color: #007bff;
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
font-size: 20px;
padding: 16px;
width: 48%;
transition: background-color 0.3s, transform 0.3s;
}
.button-group button:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
.button-group button:active {
background-color: #003d7a;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(0);
}
.message {
margin-top: 30px;
font-size: 18px;
color: #555;
}
.suggestions {
list-style: none;
padding: 0;
margin: 30px 0;
text-align: left;
}
.suggestions li {
cursor: pointer;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 15px;
padding: 18px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
transition: background-color 0.3s, color 0.3s;
}
.suggestions li:hover {
background: #e9ecef;
color: #007bff;
}
.suggestions li:active {
background: #dee2e6;
}
.remove-icon {
font-size: 20px;
color: #dc3545;
cursor: pointer;
transition: color 0.3s;
}
.remove-icon:hover {
color: #c82333;
}
.bottom-buttons {
margin-top: 30px;
}
.clear-all-button {
background-color: #dc3545;
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
font-size: 20px;
padding: 16px 24px;
margin: 10px;
transition: background-color 0.3s, transform 0.3s;
}
.clear-all-button:hover {
background-color: #c82333;
transform: translateY(-2px);
}
.clear-all-button:active {
background-color: #bd2130;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(0);
}
在浏览器中输入http://127.0.0.1:5000
就能访问写好的前段页面。
五: 前后端交互,对手机实现自动化控制
index.html界面如下图所示:
分别输入手机号、密码,然后点击
Login
按钮,就会调用login_server.py
里的trigger_adb
函数,这个函数会把登陆参数传给adb命令,在手机端自动登录。
原文链接:
Boost Developer Efficiency: Automate Android Login Workflows with ADB and Python