一、项目背景:
随着移动互联网的快速发展,Android操作系统因其开放性和灵活性,已经成为全球最流行的移动操作系统之一。然而,对于开发者、技术支持人员以及普通用户来说,Android设备的管理和调试仍然存在一定的复杂性。传统的ADB命令行工具虽然功能强大,但对于非技术用户并不友好,且在使用过程中可能存在操作风险。
为了简化Android设备的管理流程,提升用户体验,并为开发者提供更高效的调试工具,我们设计并实现了一个基于Web的Android设备管理与调试工具。本项目旨在通过现代Web技术,提供一个直观、易用的图形界面,让用户能够方便地执行操作,同时保持了足够的灵活性和扩展性,以适应不同用户群体的特定需求。
多设备支持:同时管理和调试多个连接的Android设备,支持USB线连和无线链接多样化使用;
丰富的设备管理功能:集成了设备信息查看、应用管理、屏幕截图、录屏、设备重启等实用功能并支持持续拓展。
局域网使用:支持局域网用户无线连接设备,增加了调试的灵活性。
实时反馈:通过Web界面结合投屏互动功能,用户可以通过电脑实时操作手机并看到对应的结果和设备的反馈。
安全性:代码透明可见,对应服务布在本地,方便本地使用的同时,减少了遭受网络攻击的风险。
二、技术实现:
Python
Python以其简洁和强大的特性,成为众多领域的编程首选。本项目采用Python,是因为它拥有一个活跃的社区和丰富的库支持,这极大地简化了开发流程,并提高了代码的可读性和项目的可维护性。
ADB
ADB(Android Debug Bridge)是谷歌提供的命令行工具,用于与Android设备进行通信。项目通过集成ADB接口,实现了对Android设备的高级管理和调试功能,部分代码依赖adbutils,其用法参考:adbutils
Web框架
Flask作为一个轻量级的Web应用框架,被选为构建Web界面的主要技术。它易于上手且灵活,适合快速开发Web应用。
JavaScript和前端技术
使用jQuery、Bootstrap和SweetAlert等前端技术,增强了用户界面的交互性和用户体验。
三、项目使用:
步骤1:环境准备
确保您的计算机满足以下条件:
安装Python环境,开发环境基于Python-3.12.3。
安装Pycharm,基于PyCharm Community Edition 2021.3.2。
安装Flask,可以通过pip install flask命令安装。
安装adbutils,可以通过pip install adbutils命令安装。
安装ADB(Android Debug Bridge)并配置系统的环境变量。
步骤2:运行Web应用
获取相关项目代码,
打开命令行工具(如终端、命令提示符)。
切换到项目所在的目录。
运行app.py文件来启动Flask服务器,可以通过文件路径+python app.py命令执行。
也可双击Run.bat来启动;
步骤3:访问Web界面
在启动Flask服务器后,它会在默认的Web浏览器中打开应用,或者您可以手动打开浏览器,访问http://localhost:5000。
步骤4:连接Android设备
确保您的Android设备已经开启了USB调试选项。
使用USB线将设备连接到电脑。
步骤5:使用工具功能
设备管理:
点击“刷新设备”按钮来获取当前连接的Android设备列表。
从设备列表中选择一个设备,然后点击“获取设备信息”来查看该设备的详细信息。
应用管理:
点击“获取已安装应用”按钮来列出选定设备的已安装应用。
选择一个应用,然后点击“卸载应用”按钮来卸载该应用。
使用搜索功能快速找到特定的应用。
选择设备后,点击“获取当前应用信息”来查看当前运行的应用信息。
点击“安装应用”按钮,选择一个APK文件进行安装。
设备控制:
选择设备后,点击“截图”按钮来捕获设备屏幕的图片。
点击“录屏”按钮,输入录屏时长,然后开始录制设备屏幕的视频。
点击“重启设备”按钮来远程重启选定的Android设备。
点击“启用无线调试”或“停用无线调试”按钮来开启或关闭无线调试功能。
点击“启用投屏互动模式”或“停用投屏互动模式”按钮来启动或停止scrcpy投屏功能,用户可以在电脑上镜像并控制设备屏幕,实现更高效的交互体验。
步骤6:监控日志和状态
应用运行过程中,您可以通过运行界面查看操作的状态和日志信息,以监控工具的运行情况。
步骤7:关闭应用
要关闭正在运行的Flask服务器,可以在命令行工具中使用Ctrl + C快捷键。
注意事项:
在使用过程中,如果遇到任何问题,可以参考项目的文档或寻求技术支持。
确保您的Android设备在操作过程中保持连接状态,避免断开连接导致操作失败。
四、项目框架代码及代码内容:
1、项目框架:
WebAppForAndroid
|-- app.py # Flask应用程序的主文件
|--screenshots # 截图和截屏存储文件夹
|-- adb_utils.py # 包含与 adbutils 交互的辅助函数
|-- templates/ # 前端模板文件夹
| `-- index.html # 前端模板文件
|-- static/ # 静态资源文件夹
| `-- app.js # 前端样式文件
| `-- style.css # 前端样式文件
|-- Pconfigure/ # 必要的依赖库文件
| `--scrcpy-win64-v2.4 # scrcpy-64位代码文件
| `--scrcpy-win32-v2.4 # scrcpy-32位代码文件
|-- Screenshots_Clean.py # 快速清理截图和截屏存储文件夹
|-- requirements.txt # 依赖库文件清单
|-- 安装运行环境.bat # 依赖库文件安装脚本
|-- Run.bat # 启动脚本
2、app.py代码
作为Flask应用的入口点,定义了应用的路由和视图函数。
import threading
import time
import webbrowser
import logging
import adb_utils
from flask import Flask, render_template, request, jsonify, abort
from adb_utils import uninstall_app, initialize_adb, get_new_device_info, get_windows, start_scrcpy, stop_scrcpy
# 初始化 Flask 应用
app = Flask(__name__)
# 设置应用程序的运行环境为开发模式
app.config['ENV'] = 'development'
# 启用调试模式
app.config['DEBUG'] = True
# 设置全局日志配置
logging.basicConfig(
level=logging.INFO, # 设置全局日志级别为 INFO,可以更改为其他级别
format='%(asctime)s [%(levelname)s] %(message)s', # 设置日志格式
datefmt='%Y-%m-%d %H:%M:%S' # 设置日期时间格式
)
# 初始化服务
@app.before_first_request
def on_first_request():
if not get_windows():
# 如果不是Windows系统,终止初始化
abort(400, "抱歉,当前应用仅支持Windows系统!!!")
initialize_adb()
# 主页视图
@app.route('/')
def index():
devices = adb_utils.get_devices()
return render_template('index.html', devices=devices)
# 刷新设备列表
@app.route('/refresh', methods=['POST'])
def refresh():
devices = adb_utils.get_devices()
return jsonify(devices)
# 读取已安装应用
@app.route('/get_apps', methods=['POST'])
def get_apps():
device_id = request.form.get('device_id')
apps = adb_utils.get_installed_apps(device_id)
return jsonify(apps)
# 卸载应用
@app.route('/uninstall_app', methods=['POST'])
def uninstall_app_route():
device_id = request.form.get('device_id')
package_name = request.form.get('package_name')
result = uninstall_app(device_id, package_name)
return jsonify(result)
# 安装 APK
@app.route('/install_apk', methods=['POST'])
def install_apk():
device_id = request.form.get('device_id')
apk_file = request.files.get('apk_file')
result = adb_utils.install_apk(device_id, apk_file)
return jsonify(result)
# 截图
@app.route('/screenshot', methods=['POST'])
def screenshot():
device_id = request.form.get('device_id')
result = adb_utils.take_screenshot(device_id)
return jsonify(result)
# 录屏
@app.route('/record_screen', methods=['POST'])
def record_screen():
device_id = request.form.get('device_id')
duration = request.form.get('duration')
result = adb_utils.record_screen(device_id, duration)
return jsonify(result)
# 重启设备
@app.route('/restart_device', methods=['POST'])
def restart_device():
device_id = request.form.get('device_id')
result = adb_utils.restart_device(device_id)
return jsonify(result)
# 获取设备信息
@app.route('/get_device_info', methods=['POST'])
def get_device_info_route():
device_id = request.form.get('device_id')
if not device_id:
return jsonify({'success': False, 'error': 'Device ID not provided.'})
try:
device_info = get_new_device_info(device_id)
if device_info:
return jsonify({'success': True, **device_info})
else:
return jsonify({'success': False, 'error': 'Failed to get device info.'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
# 当前正在运行的应用信息
@app.route('/get_current_app_info', methods=['POST'])
def get_current_app_info_route():
device_id = request.form.get('device_id')
info = adb_utils.get_current_app_info(device_id)
return jsonify(info)
# 启用无线调试
@app.route('/enable_wireless_debugging', methods=['POST'])
def enable_wireless_debugging():
device_id = request.form.get('device_id')
result = adb_utils.enable_wireless_debugging(device_id)
return jsonify(result)
# 禁用无线调试
@app.route('/disable_wireless_debugging', methods=['POST'])
def disable_wireless_debugging():
device_id = request.form.get('device_id')
result = adb_utils.disable_wireless_debugging(device_id)
return jsonify(result)
# # 启用投屏互动模式
# @app.route('/scrcpy', methods=['POST'])
# def start_scrcpy():
# device_id = request.form.get('device_id')
# if not device_id:
# return jsonify({'success': False, 'error': 'Device ID not provided.'})
#
# try:
# # 指定 scrcpy.exe 的完整路径
# scrcpy_exe_path = r'D:\Projectx\WebAppForAndroid\Pconfigure\scrcpy-win64-v2.4\scrcpy.exe'
#
# # 执行 scrcpy 命令,指定设备 ID
# scrcpy_command = f'"{scrcpy_exe_path}" -s {device_id}'
# subprocess.Popen(scrcpy_command, shell=True)
#
# return jsonify({'success': True})
# except Exception as e:
# return jsonify({'success': False, 'error': str(e)})
# # 启用投屏互动模式
# @app.route('/scrcpy', methods=['POST'])
# def start_scrcpy():
# script_dir_scrcpy = os.path.dirname(os.path.abspath(__file__))
# logging.info(f"当前脚本所在目录: {script_dir_scrcpy}")
# device_id = request.form.get('device_id')
# logging.info(f"当前获取的设备ID为: {device_id}")
# if not device_id:
# return jsonify({'success': False, 'error': 'Device ID not provided.'})
#
# try:
# # 检测系统架构
# system_bits = platform.architecture()[0]
# logging.info(f"系统位数: {system_bits}")
#
# # 构建 scrcpy-win32-v2.4 和 scrcpy-win64-v2.4 的完整路径
# scrcpy_exe_path_32 = os.path.join(script_dir_scrcpy, "Pconfigure", "scrcpy-win32-v2.4", "scrcpy.exe")
# scrcpy_exe_path_64 = os.path.join(script_dir_scrcpy, "Pconfigure", "scrcpy-win64-v2.4", "scrcpy.exe")
#
# # 选择正确的scrcpy工具相对路径
# scrcpy_exe_path = scrcpy_exe_path_64 if system_bits == '64bit' else scrcpy_exe_path_32
# logging.info(f"选择的scrcpy路径: {scrcpy_exe_path}")
#
# # 指定 scrcpy.exe 的完整路径
# # scrcpy_exe_path = r'D:\Projectx\WebAppForAndroid\Pconfigure\scrcpy-win64-v2.4\scrcpy.exe'
#
# # 执行 scrcpy 命令,指定设备 ID
# scrcpy_command = f'"{scrcpy_exe_path}" -s {device_id}'
# subprocess.Popen(scrcpy_command, shell=True)
#
# return jsonify({'success': True})
# except Exception as e:
# return jsonify({'success': False, 'error': str(e)})
#
#
# # 停用投屏互动模式
# @app.route('/stop_scrcpy', methods=['POST'])
# def stop_scrcpy():
# try:
# # 获取 scrcpy.exe 进程
# for proc in psutil.process_iter():
# if proc.name() == 'scrcpy.exe':
# # 终止 scrcpy.exe 进程
# proc.terminate()
# return jsonify({'success': True})
#
# # 如果没有找到 scrcpy.exe 进程,返回失败信息
# return jsonify({'success': False, 'error': 'scrcpy 进程未找到'})
# except Exception as e:
# return jsonify({'success': False, 'error': str(e)})
# 启用投屏互动模式
@app.route('/scrcpy', methods=['POST'])
def start_scrcpy_route():
device_id = request.form.get('device_id')
if not device_id:
return jsonify({'success': False, 'error': 'Device ID not provided.'}), 400
result, status_code = start_scrcpy(device_id)
return jsonify(result), status_code
# 停用投屏互动模式
@app.route('/stop_scrcpy', methods=['POST'])
def stop_scrcpy_route():
result, status_code = stop_scrcpy()
return jsonify(result), status_code
# 定义一个打开网页的函数
def open_browser():
# 使用延迟确保 Flask 应用程序完全启动
time.sleep(1)
webbrowser.open_new_tab('http://localhost:5000')
if __name__ == '__main__':
# 在 Flask 应用程序启动之前创建一个线程来打开浏览器
if not hasattr(app, 'browser_opened') or not app.browser_opened:
browser_thread = threading.Thread(target=open_browser)
browser_thread.start()
app.browser_opened = True # 标记浏览器已经打开
# 运行 Flask 应用程序
app.run(host='0.0.0.0', port=5000, use_reloader=False)
3、adb_utils.py代码
封装与ADB接口交互的所有辅助函数,如设备连接、应用管理等。
import os
import shutil
import adbutils
import logging
import time
import subprocess
import tempfile
import re
import platform
import psutil
# 设置全局日志配置
logging.basicConfig(
level=logging.INFO, # 设置全局日志级别为 INFO,可以更改为其他级别
format='%(asctime)s [%(levelname)s] %(message)s', # 设置日志格式
datefmt='%Y-%m-%d %H:%M:%S' # 设置日期时间格式
)
def create_storage_path():
"""
创建截图存录屏储路径
"""
# 获取当前脚本所在的目录
script_dir = os.path.dirname(os.path.abspath(__file__))
# 定义存储路径为脚本所在目录下的 /screenshots 文件夹
storage_path = os.path.join(script_dir, 'screenshots')
# 确保存储目录存在
if not os.path.exists(storage_path):
logging.info(f'创建截图录屏存储路径: {storage_path}')
os.makedirs(storage_path)
# 返回存储路径
return storage_path
# 调用函数创建截图存储路径,并将返回值存储在变量中
storage_upath = create_storage_path()
# # 初始化adb服务
# def initialize_adb():
# logging.info('正在初始化 ADB 服务...')
# subprocess.run(['adb', 'start-server'])
#
# # 检查 ADB 服务器状态
# result = subprocess.run(['adb', 'devices'], capture_output=True, text=True)
# if 'List of devices attached' not in result.stdout:
# raise Exception('Failed to connect to ADB server.')
# logging.info('ADB 服务初始化完毕')
def get_windows():
return platform.system().lower() == 'windows'
def initialize_adb():
# 获取当前脚本所在的目录
script_dir_adb = os.path.dirname(os.path.abspath(__file__))
logging.info(f"当前脚本所在目录: {script_dir_adb}")
# 查询当前环境变量是否配置了adb
adb_in_path = shutil.which("adb")
if adb_in_path:
adb_path = adb_in_path
else:
# 检测系统架构
system_bits = platform.architecture()[0]
logging.info(f"系统位数: {system_bits}")
# 构建 scrcpy-win32-v2.4 和 scrcpy-win64-v2.4 的完整路径
adb_path_32 = os.path.join(script_dir_adb, "Pconfigure", "scrcpy-win32-v2.4", "adb")
adb_path_64 = os.path.join(script_dir_adb, "Pconfigure", "scrcpy-win64-v2.4", "adb")
# 选择正确的 adb 工具路径
adb_path = adb_path_64 if system_bits == '64bit' else adb_path_32
logging.info(f"选择的ADB路径: {adb_path}")
# 初始化adb服务
logging.info('正在初始化 ADB 服务...')
subprocess.run([adb_path, 'start-server'])
# 检查 ADB 服务器状态
result = subprocess.run([adb_path, 'devices'], capture_output=True, text=True)
if 'List of devices attached' not in result.stdout:
raise Exception('Failed to connect to ADB server.')
logging.info('ADB 服务初始化完毕')
# 获取设备列表
def get_devices():
try:
logging.info('正在获取设备列表...')
client = adbutils.AdbClient()
devices = client.device_list() # 使用 device_list 方法
device_serials = [device.serial for device in devices]
logging.info(f'获取到的设备列表: {device_serials}')
return device_serials
except Exception as e:
logging.error(f'获取设备列表失败: {e}')
return []
# 获取已安装应用
def get_installed_apps(device_id):
try:
logging.info(f'正在获取设备 {device_id} 上的已安装应用...')
device = adbutils.adb.device(device_id)
packages = device.list_packages()
logging.info(f'设备 {device_id} 上的已安装应用: {packages}')
return packages
except Exception as e:
logging.error(f'获取已安装应用失败: {e}')
return []
# 卸载应用
def uninstall_app(device_id, package_name):
try:
logging.info(f'正在卸载设备 {device_id} 上的应用 {package_name}...')
if not device_id or not package_name:
logging.warning('设备 ID 和包名是必需的')
return {'success': False, 'error': '设备 ID 和包名是必需的'}
device = adbutils.adb.device(device_id)
device.uninstall(package_name)
logging.info(f'应用 {package_name} 卸载成功')
return {'success': True}
except Exception as e:
logging.error(f'卸载应用失败: {e}')
return {'success': False, 'error': str(e)}
# 安装应用
def install_apk(device_id, apk_file):
try:
logging.info(f'正在为设备 {device_id} 安装 APK 文件 {apk_file.filename}...')
# 检查文件是否是 .apk 格式
if not apk_file.filename.endswith('.apk'):
logging.warning('文件格式错误:只能安装 .apk 格式的文件')
return {'success': False, 'error': '文件格式错误:只能安装 .apk 格式的文件'}
# 通过 adbutils 获取设备对象
device = adbutils.adb.device(device_id)
# 使用临时文件夹保存 APK 文件
with tempfile.TemporaryDirectory() as temp_dir:
apk_filename = apk_file.filename
apk_path = os.path.join(temp_dir, apk_filename)
# 将 APK 文件保存到临时文件夹
apk_file.save(apk_path)
logging.info(f'APK 文件已保存到临时文件夹: {apk_path}')
# 检查 APK 文件是否存在
if not os.path.exists(apk_path):
raise FileNotFoundError(f"找不到指定的 APK 文件:{apk_path}")
# 使用 adbutils 进行 APK 安装
device.install(apk_path)
logging.info(f'APK 文件 {apk_path} 安装成功')
# 返回安装成功的结果
return {'success': True}
except FileNotFoundError as e:
logging.error(f'安装 APK 失败: {e}')
return {'success': False, 'error': str(e)}
except Exception as e:
logging.error(f'安装 APK 失败: {e}')
return {'success': False, 'error': str(e)}
# 截图
def take_screenshot(device_id):
try:
logging.info(f'正在为设备 {device_id} 截图...')
device = adbutils.adb.device(device_id)
# 检查屏幕是否亮屏
if not device.is_screen_on():
logging.error('设备屏幕未亮,无法进行截图。')
return {'success': False, 'error': '设备屏幕未亮,无法进行截图。'}
timestamp = time.strftime('%Y%m%d%H%M%S')
local_screenshot_path = os.path.join(storage_upath, f'screenshot_{timestamp}.png')
pil_image = device.screenshot()
pil_image.save(local_screenshot_path)
logging.info(f'截图保存至: {local_screenshot_path}')
return {'success': True, 'path': local_screenshot_path}
except Exception as e:
logging.error(f'截图失败: {e}')
return {'success': False, 'error': str(e)}
# 录屏函数
def record_screen(device_id, duration):
try:
logging.info(f'正在为设备 {device_id} 录屏,时长: {duration} 秒...')
duration = int(duration)
device = adbutils.adb.device(device_id)
# 检查屏幕是否亮屏
if not device.is_screen_on():
logging.error('设备屏幕未亮,无法进行录屏。')
return {'success': False, 'error': '设备屏幕未亮,无法进行录屏。'}
timestamp = time.strftime('%Y%m%d%H%M%S')
device_record_path = f'/sdcard/screen_record_{timestamp}.mp4'
screenrecord_command = f"screenrecord --time-limit {duration} {device_record_path}"
device.shell(screenrecord_command)
local_record_path = os.path.join(storage_upath, f'screen_record_{timestamp}.mp4')
device.sync.pull(device_record_path, local_record_path)
device.shell(f'rm {device_record_path}')
logging.info(f'录屏完成,保存至: {local_record_path}')
return {'success': True, 'path': local_record_path}
except Exception as e:
logging.error(f'录屏失败: {e}')
return {'success': False, 'error': str(e)}
# 重启设备
def restart_device(device_id):
try:
logging.info(f'正在重启设备 {device_id}...')
device = adbutils.adb.device(device_id)
device.reboot()
logging.info(f'设备 {device_id} 重启成功')
return {'success': True}
except Exception as e:
logging.error(f'重启设备失败: {e}')
return {'success': False, 'error': str(e)}
# 获取设备信息
def get_new_device_info(device_id):
try:
logging.info(f'正在获取设备 {device_id} 的信息...')
# 使用 adb 命令获取设备信息
command = f'adb -s {device_id} shell getprop'
logging.info(f'执行命令: {command}')
output = subprocess.check_output(command, shell=True, encoding='utf-8')
logging.debug(f'ADB 输出: \n{output}')
# 默认需要获取的参数列表(MTK芯片)
desired_properties_mtk = {
'ro.product.model': '设备名称',
'ro.product.brand': '设备品牌',
'ro.build.version.release': '安卓版本',
'ro.build.version.sdk': 'SDK版本',
'ro.soc.manufacturer': 'CPU品牌',
'ro.boot.hardware': 'CPU型号',
'ro.product.cpu.abi': 'CPU架构',
'ro.serialno': '设备序列号'
}
# 高通芯片上的需要获取的参数列表
desired_properties_qualcomm = {
'ro.product.model': '设备名称',
'ro.product.brand': '设备品牌',
'ro.build.version.release': '安卓版本',
'ro.build.version.sdk': 'SDK版本',
'ro.soc.manufacturer': 'CPU品牌',
'ro.boot.hardware.platform': 'CPU型号',
'ro.product.cpu.abi': 'CPU架构',
'ro.serialno': '设备序列号'
}
# 将输出解析成字典
device_info = {}
is_mtk = False
is_qualcomm = False
for line in output.strip().split('\n'):
# 将行分割成键值对
if ':' in line:
key, value = line.split(':', 1)
# 移除前后的空格和特殊符号
key = key.strip().strip('[]')
value = value.strip().strip('[]')
# 检查当前键值对是否与MTK芯片或高通芯片匹配
if key in desired_properties_mtk:
device_info[desired_properties_mtk[key]] = value
is_mtk = True
elif key in desired_properties_qualcomm:
device_info[desired_properties_qualcomm[key]] = value
is_qualcomm = True
# 根据芯片品牌选择合适的字典
# 如果既不是 MTK 也不是高通,则选择默认的 desired_properties_mtk
if is_qualcomm:
desired_properties = desired_properties_qualcomm
elif is_mtk:
desired_properties = desired_properties_mtk
else:
# 未能确定芯片类型,返回空字典或默认字典
logging.warning('未能确定芯片类型')
return {'success': False, 'error': '未能确定芯片类型'}
# 检查是否获取到了所有需要的参数
for prop in desired_properties.values():
# 如果某个属性缺失,则将其设置为 None
if prop not in device_info:
device_info[prop] = None
logging.info(f'获取到的设备信息: {device_info}')
return device_info
except subprocess.CalledProcessError as e:
logging.error(f'Error fetching device info: {e}')
return {'error': 'Failed to execute adb command.'}
except Exception as e:
logging.error(f'Unexpected error: {e}')
return {'error': f'Unexpected error: {e}'}
# 获取当前正在运行的应用信息
def get_current_app_info(device_id):
try:
logging.info(f'正在获取设备 {device_id} 当前运行的应用信息...')
# 使用 adbutils 获取设备
device = adbutils.adb.device(device_id)
# 检查屏幕是否亮屏
if not device.is_screen_on():
logging.error('设备屏幕未亮,无法获取当前应用信息。')
return {'success': False, 'error': '设备屏幕未亮,无法获取当前应用信息。'}
# 使用命令获取当前聚焦窗口的信息
focused_window_info = device.shell("dumpsys window | grep mFocusedWindow")
# 如果找到聚焦窗口信息
if focused_window_info:
# 提取包名和活动名的部分
parts = focused_window_info.split()
if len(parts) >= 3:
package_and_activity = parts[-1].rstrip('}') # 移除末尾的 '}'
# 检查 package_and_activity 是否包含 '/'
if '/' in package_and_activity:
package_name, activity_name = package_and_activity.split('/')
# 返回信息
logging.info(f'获取到的当前运行的应用: 包名: {package_name}, 活动名: {activity_name}')
return {
'success': True,
'package_name': package_name,
'activity_name': activity_name,
'start_activity_name': package_and_activity, # 完整的启动活动名
}
# 如果未找到 '/',返回错误信息
logging.warning('未找到当前运行的应用')
return {'success': False, 'error': '未找到当前运行的应用'}
except Exception as e:
# 在发生异常时记录错误并返回错误信息
logging.error(f'获取当前应用信息失败: {e}')
return {'success': False, 'error': str(e)}
# 获取设备IP地址列表
def get_device_ip_address(device_id):
try:
device = adbutils.adb.device(device_id)
# 获取设备的IP地址
ip_output = device.shell('ip addr show wlan0').strip()
ip_addresses = re.findall(r'inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', ip_output)
return ip_addresses
except Exception as e:
logging.error(f"获取设备 IP 地址失败: {e}")
return []
# 开启无线调试
def enable_wireless_debugging(device_id, max_retries=2):
try:
logging.info(f"正在为设备 {device_id} 启用无线调试...")
device = adbutils.adb.device(device_id)
device.shell('setprop service.adb.tcp.port 5555')
logging.info("设置 service.adb.tcp.port 为 5555")
device.shell('stop adbd')
logging.info("停止 adb 服务")
device.shell('start adbd')
logging.info("启动 adb 服务")
# 获取设备IP地址列表
ip_addresses = get_device_ip_address(device_id)
if not ip_addresses:
logging.warning("无法获取设备 IP 地址,无法连接到无线调试端口")
return {'success': False, 'error': '无法获取设备 IP 地址'}
# 尝试连接对应的IP地址
retries = 0
for ip_addr in ip_addresses:
port = '5555'
connect_cmd = f"adb connect {ip_addr}:{port}"
try:
subprocess.run(connect_cmd, shell=True, check=True)
logging.info(f"已连接到设备的无线调试端口: {ip_addr}:{port}")
# 发送验证命令
verify_cmd = f"adb -s {device_id} shell echo hello"
verify_result = subprocess.run(verify_cmd, shell=True, capture_output=True)
if verify_result.returncode == 0:
logging.info("设备处于无线调试状态")
return {'success': True}
else:
logging.warning("设备未能正常响应验证命令,可能无线调试失败")
return {'success': False, 'error': '设备未能正常响应验证命令,可能无线调试失败'}
except subprocess.CalledProcessError as e:
if "10061" in str(e): # 目标计算机积极拒绝连接错误
# 重新设置TCP端口
device.shell('setprop service.adb.tcp.port 5555')
logging.info("重新设置 service.adb.tcp.port 为 5555")
retries += 1
if retries >= max_retries:
logging.warning(f"已达到最大重试次数 ({max_retries}), 无法连接到任何 IP 地址的无线调试端口")
return {'success': False, 'error': f"已达到最大重试次数 ({max_retries}), 无法连接到无线调试端口"}
else:
continue # 继续尝试连接
logging.warning(f"无法连接到 {ip_addr}:{port}, 错误: {e}")
logging.warning("无法连接到任何 IP 地址的无线调试端口")
return {'success': False, 'error': '无法连接到无线调试端口'}
except subprocess.CalledProcessError as e:
logging.error(f"执行 adb 命令失败: {e}")
return {'success': False, 'error': str(e)}
except Exception as e:
logging.error(f"启用无线调试失败: {e}")
return {'success': False, 'error': str(e)}
# 关闭无线调试
def disable_wireless_debugging(device_id, max_retries=3):
try:
logging.info(f"正在为设备 {device_id} 禁用无线调试...")
device = adbutils.adb.device(device_id)
# 获取设备IP地址列表
ip_addresses = get_device_ip_address(device_id)
if not ip_addresses:
logging.warning("无法获取设备 IP 地址,无法断开无线调试连接")
return {'success': False, 'error': '无法获取设备 IP 地址'}
port = '5555'
# 关闭设备端的无线调试选项
device.shell('stop adbd')
logging.info("停止 adb 服务")
device.shell('start adbd')
logging.info("启动 adb 服务")
# 断开对应IP地址的无线连接
retries = 0
for ip_addr in ip_addresses:
disconnect_cmd = f"adb disconnect {ip_addr}:{port}"
try:
subprocess.run(disconnect_cmd, shell=True, check=True)
logging.info(f"已断开与设备 {ip_addr}:{port} 的无线连接")
except subprocess.CalledProcessError as e:
if "10061" in str(e): # 目标计算机积极拒绝连接错误
# 重新设置TCP端口
device.shell('setprop service.adb.tcp.port 5555')
logging.info("重新设置 service.adb.tcp.port 为 5555")
retries += 1
if retries >= max_retries:
logging.warning(f"已达到最大重试次数 ({max_retries}), 无法断开与任何 IP 地址的无线连接")
return {'success': False, 'error': f"已达到最大重试次数 ({max_retries}), 无法断开与任何 IP 地址的无线连接"}
else:
continue # 继续断开连接
logging.warning(f"无法断开与 {ip_addr}:{port} 的连接, 错误: {e}")
logging.info("无线调试已禁用")
return {'success': True}
except subprocess.CalledProcessError as e:
logging.error(f"执行 adb 命令失败: {e}")
return {'success': False, 'error': str(e)}
except Exception as e:
logging.error(f"禁用无线调试失败: {e}")
return {'success': False, 'error': str(e)}
# 获取scrcpy启动路径
def get_scrcpy_executable_path():
script_dir = os.path.dirname(os.path.abspath(__file__))
system_bits = platform.architecture()[0]
base_path = os.path.join(script_dir, "Pconfigure")
if system_bits == '64bit':
return os.path.join(base_path, "scrcpy-win64-v2.4", "scrcpy.exe")
else:
return os.path.join(base_path, "scrcpy-win32-v2.4", "scrcpy.exe")
# 启用互动投屏功能
def start_scrcpy(device_id):
try:
scrcpy_path = get_scrcpy_executable_path()
logging.info(f"选择的scrcpy路径: {scrcpy_path}")
command = [scrcpy_path, '-s', str(device_id)]
subprocess.Popen(command, shell=True)
logging.info("scrcpy 启动成功")
return {'success': True}, 200
except Exception as e:
logging.exception("启动scrcpy失败")
return {'success': False, 'error': str(e)}, 500
# 停用互动投屏功能
def stop_scrcpy():
try:
for proc in psutil.process_iter(['pid', 'name']):
# 使用 name() 方法获取进程名称
if proc.name() == 'scrcpy.exe':
proc.terminate()
return {'success': True}, 200
# 如果没有找到名为'scrcpy.exe'的进程
return {'success': False, 'error': 'scrcpy 进程未找到'}, 200
except Exception as e:
logging.exception("停止scrcpy失败")
return {'success': False, 'error': str(e)}, 500
4、Screenshots_Clean.py代码
提供了快速清理截图和截屏存储文件夹功能;
import os
import logging
def clear_screenshots_folder():
# 获取当前脚本所在的目录
current_script_path = os.path.abspath(__file__)
current_project_path = os.path.dirname(current_script_path)
# 定位到 screenshots 文件夹
screenshots_folder_path = os.path.join(current_project_path, 'screenshots')
# 检查文件夹是否存在
if not os.path.exists(screenshots_folder_path):
logging.warning(f"文件夹 {screenshots_folder_path} 不存在")
return
# 遍历 screenshots 文件夹下的所有文件
for filename in os.listdir(screenshots_folder_path):
file_path = os.path.join(screenshots_folder_path, filename)
# 删除文件
try:
if os.path.isfile(file_path):
os.remove(file_path)
logging.info(f"已删除文件: {file_path}")
except Exception as e:
logging.error(f"删除文件时出错: {file_path} - 错误信息: {e}")
logging.info("截图和录屏文件已清理完毕")
if __name__ == "__main__":
# 配置日志输出格式
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 执行清理操作
clear_screenshots_folder()
5、index.html代码
提供了用户界面,使用户能够通过Web界面与后端进行交互。
<!DOCTYPE html>
<html>
<head>
<title>Android 调试工具</title>
<!-- 引入 Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<!-- 引入 FontAwesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- 引入样式文件 -->
<link rel="stylesheet" type="text/css" href="/static/style.css">
<!-- 引入 SweetAlert 的 CSS 和 JS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js"></script>
</head>
<body class="bg-light">
<header class="container my-4">
<div class="text-center">
<h1>Android 调试工具</h1>
</div>
</header>
<main class="container">
<!-- 使用 Bootstrap 的网格系统布局 -->
<div class="row">
<!-- 设备管理部分 -->
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header">
<h2 class="card-title">设备管理</h2>
</div>
<div class="card-body">
<select id="device-select" class="form-select mb-2"></select>
<button id="refresh-devices-button" class="btn btn-primary w-100 mb-2" onclick="refreshDevices()">刷新设备</button>
<button id="get-device-info-button" class="btn btn-success w-100 mb-2" onclick="getDeviceInfo()">获取设备信息</button>
<div id="device-info" class="border p-3"></div>
</div>
</div>
</div>
<!-- 应用管理部分 -->
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header">
<h2 class="card-title">应用管理</h2>
</div>
<div class="card-body">
<button id="get-installed-apps-button" class="btn btn-info w-100 mb-2" onclick="getInstalledApps()">获取已安装应用</button>
<select id="app-select" class="form-select mb-2"></select>
<button id="uninstall-app-button" class="btn btn-danger w-100 mb-2" onclick="uninstallApp()">卸载应用</button>
<button id="get-current-app-info-button" class="btn btn-warning w-100 mb-2" onclick="getCurrentAppInfo()">获取当前应用信息</button>
<div id="current-app-info" class="border p-3 mb-3"></div>
<input type="file" id="apk-file" accept=".apk" class="form-control mb-2">
<button id="install-apk-button" class="btn btn-secondary w-100" onclick="installApk()">安装应用</button>
</div>
</div>
</div>
<!-- 设备控制部分 -->
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header">
<h2 class="card-title">设备控制</h2>
</div>
<div class="card-body">
<button id="take-screenshot-button" class="btn btn-warning w-100 mb-2" onclick="takeScreenshot()">截图</button>
<button id="record-screen-button" class="btn btn-primary w-100 mb-2" onclick="recordScreen()">录屏</button>
<button id="restart-device-button" class="btn btn-danger w-100 mb-2" onclick="restartDevice()">重启设备</button>
<button id="enable-wireless-debugging-button" class="btn btn-success w-100 mb-2" onclick="enableWirelessDebugging()">启用无线调试</button>
<button id="disable-wireless-debugging-button" class="btn btn-danger w-100 mb-2" onclick="disableWirelessDebugging()">停用无线调试</button>
<button id="start-scrcpy-button" class="btn btn-primary w-100 mb-2" onclick="startScrcpy()">启用投屏互动模式</button>
<button id="stop-scrcpy-button" class="btn btn-danger w-100 mb-2" onclick="stopScrcpy()">停用投屏互动模式</button>
</div>
</div>
</div>
</div>
</main>
<!-- 引入 jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- 引入 Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.min.js"></script>
<!-- 引入JS 脚本 -->
<script src="/static/app.js"></script>
</body>
</html>
6、app.js代码
包含了处理用户交互的JavaScript代码,如AJAX请求和用户界面更新。
// 刷新设备列表
function refreshDevices() {
const refreshDevicesButton = $('#refresh-devices-button');
refreshDevicesButton.prop('disabled', true);
$.post('/refresh', function(data) {
const deviceSelect = $('#device-select');
deviceSelect.empty();
if (data.length === 0) {
Swal.fire({
icon: 'warning',
title: '未检测到设备',
text: '请检查连接。',
confirmButtonText: '确定'
});
} else {
deviceSelect.html(data.map(device => `<option value="${device}">${device}</option>`).join(''));
}
}).fail(function() {
Swal.fire({
icon: 'error',
title: '刷新设备列表时发生错误',
text: '请检查网络连接。',
confirmButtonText: '确定'
});
}).always(function() {
refreshDevicesButton.prop('disabled', false);
});
}
// 获取设备信息
function getDeviceInfo() {
const getDeviceInfoButton = $('#get-device-info-button');
getDeviceInfoButton.prop('disabled', true);
const deviceId = $('#device-select').val();
if (!deviceId) {
Swal.fire({
icon: 'warning',
title: '请选择设备',
text: '请选择设备。',
confirmButtonText: '确定'
});
getDeviceInfoButton.prop('disabled', false);
return;
}
$.post('/get_device_info', { device_id: deviceId })
.done(function(data) {
if (data.error) {
Swal.fire({
icon: 'error',
title: '获取设备信息失败',
text: data.error,
confirmButtonText: '确定'
});
} else {
displayDeviceInfo(data);
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: '获取设备信息时发生错误',
confirmButtonText: '确定'
});
})
.always(function() {
getDeviceInfoButton.prop('disabled', false);
});
}
// 显示设备信息
function displayDeviceInfo(deviceInfo) {
const deviceInfoDiv = $('#device-info');
deviceInfoDiv.empty();
const displayOrder = [
'设备名称',
'设备品牌',
'安卓版本',
'SDK版本',
'CPU品牌',
'CPU型号',
'CPU架构',
'设备序列号'
];
const htmlContent = displayOrder.map(key => {
if (deviceInfo.hasOwnProperty(key)) {
return `<div><strong>${key}:</strong> ${deviceInfo[key]}</div>`;
}
return '';
}).join('');
deviceInfoDiv.append(htmlContent);
}
// 假设 installedApps 是一个数组,包含所有已安装的应用包名
let installedApps = [];
// 初始化已安装应用列表
function getInstalledApps() {
const getInstalledAppsButton = $('#get-installed-apps-button');
getInstalledAppsButton.prop('disabled', true);
const deviceId = $('#device-select').val();
$.post('/get_apps', { device_id: deviceId }, function(data) {
if (data && data.length > 0) {
installedApps = data; // 将获取到的应用包名列表存储到 installedApps 中
updateAppSelect(installedApps);
} else {
Swal.fire({
icon: 'warning',
title: '未能获取到已安装的应用程序',
confirmButtonText: '确定'
});
}
}).fail(function() {
Swal.fire({
icon: 'error',
title: '获取已安装的应用程序失败',
confirmButtonText: '确定'
});
}).always(function() {
getInstalledAppsButton.prop('disabled', false);
});
}
// 更新选择框内容
function updateAppSelect(apps) {
const appSelect = $('#app-select');
appSelect.empty();
apps.forEach(app => {
const option = new Option(app, app);
appSelect.append(option);
});
}
// 在输入事件中调用 searchApps
$('#app-search-input').on('input', function() {
const query = $(this).val().toLowerCase();
searchApps(query);
});
// 使用 filter 方法实现搜索
function searchApps(query) {
// 根据查询参数在 installedApps 列表中过滤包名
const filteredApps = installedApps.filter(app => app.toLowerCase().includes(query));
// 将过滤后的结果显示在选择框中
updateAppSelect(filteredApps);
}
// 卸载应用程序
function uninstallApp() {
const uninstallAppButton = $('#uninstall-app-button');
uninstallAppButton.prop('disabled', true);
const deviceId = $('#device-select').val();
const packageName = $('#app-select').val();
if (!deviceId || !packageName) {
Swal.fire({
icon: 'warning',
title: '请选择要卸载的设备和应用程序。',
confirmButtonText: '确定'
});
uninstallAppButton.prop('disabled', false);
return;
}
// 二次确认
Swal.fire({
title: '确认',
text: `您确定要卸载包名为 ${packageName} 的应用程序吗?`,
icon: 'question',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
$.post('/uninstall_app', { device_id: deviceId, package_name: packageName })
.done(function(response) {
if (response.success) {
Swal.fire({
icon: 'success',
title: '应用程序卸载成功。',
confirmButtonText: '确定'
});
getInstalledApps();
} else {
Swal.fire({
icon: 'error',
title: '无法卸载应用',
text: response.error,
confirmButtonText: '确定'
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: '错误',
text: '无法卸载应用程序。',
confirmButtonText: '确定'
});
})
.always(function() {
uninstallAppButton.prop('disabled', false);
});
} else {
uninstallAppButton.prop('disabled', false);
}
});
}
// 安装 APK 文件
function installApk() {
const installApkButton = $('#install-apk-button');
installApkButton.prop('disabled', true);
const deviceId = $('#device-select').val();
const apkFile = $('#apk-file')[0].files[0];
if (!apkFile) {
Swal.fire({
icon: 'warning',
title: '请先选择一个 APK 文件。',
confirmButtonText: '确定'
});
installApkButton.prop('disabled', false);
return;
}
if (!apkFile.name.toLowerCase().endsWith('.apk')) {
Swal.fire({
icon: 'warning',
title: '请选择一个 .apk 文件。',
confirmButtonText: '确定'
});
installApkButton.prop('disabled', false);
return;
}
const formData = new FormData();
formData.append('device_id', deviceId);
formData.append('apk_file', apkFile);
$.ajax({
url: '/install_apk',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(data) {
Swal.fire({
icon: data.success ? 'success' : 'error',
title: data.success ? 'APK 安装成功' : 'APK 安装失败',
text: data.success ? '' : data.error,
confirmButtonText: '确定'
});
},
error: function() {
Swal.fire({
icon: 'error',
title: '错误',
text: '无法安装 APK',
confirmButtonText: '确定'
});
}
}).always(function() {
installApkButton.prop('disabled', false);
});
}
// 截图函数
function takeScreenshot() {
const takeScreenshotButton = $('#take-screenshot-button');
takeScreenshotButton.prop('disabled', true);
const deviceId = $('#device-select').val();
$.post('/screenshot', { device_id: deviceId }, function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: '截图成功!',
text: `路径:${data.path}`,
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '截图失败',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
takeScreenshotButton.prop('disabled', false);
});
}
// 屏幕录制函数
function recordScreen() {
const recordScreenButton = $('#record-screen-button');
recordScreenButton.prop('disabled', true);
const deviceId = $('#device-select').val();
const duration = prompt('请输入录屏时长(秒):');
if (!duration) {
Swal.fire({
icon: 'warning',
title: '请输入录屏时长。',
confirmButtonText: '确定'
});
recordScreenButton.prop('disabled', false);
return;
}
$.post('/record_screen', { device_id: deviceId, duration: duration }, function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: '录屏成功!',
text: `路径:${data.path}`,
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '录屏失败',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
recordScreenButton.prop('disabled', false);
});
}
// 重启设备函数
function restartDevice() {
const restartDeviceButton = $('#restart-device-button');
restartDeviceButton.prop('disabled', true);
const deviceId = $('#device-select').val();
$.post('/restart_device', { device_id: deviceId }, function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: '设备已重启。',
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '设备重启失败',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
restartDeviceButton.prop('disabled', false);
});
}
// 获取当前应用程序信息
function getCurrentAppInfo() {
const getCurrentAppInfoButton = $('#get-current-app-info-button');
getCurrentAppInfoButton.prop('disabled', true);
const deviceId = $('#device-select').val();
if (!deviceId) {
Swal.fire({
icon: 'warning',
title: '请选择设备。',
confirmButtonText: '确定'
});
getCurrentAppInfoButton.prop('disabled', false);
return;
}
$.post('/get_current_app_info', { device_id: deviceId }, function(data) {
const appInfoDiv = $('#current-app-info');
appInfoDiv.empty();
if (data.success) {
appInfoDiv.append(`<div><strong>Package Name:</strong> ${data.package_name}</div>`);
appInfoDiv.append(`<div><strong>Activity Name:</strong> ${data.activity_name}</div>`);
appInfoDiv.append(`<div><strong>Start Activity Name:</strong> ${data.start_activity_name}</div>`);
} else {
Swal.fire({
icon: 'error',
title: '获取当前应用信息失败',
text: data.error,
confirmButtonText: '确定'
});
}
}).fail(function() {
Swal.fire({
icon: 'error',
title: '错误',
text: '无法获取当前应用程序信息!',
confirmButtonText: '确定'
});
}).always(function() {
getCurrentAppInfoButton.prop('disabled', false);
});
}
// 启用无线调试
function enableWirelessDebugging() {
const enableWirelessDebuggingButton = $('#enable-wireless-debugging-button');
enableWirelessDebuggingButton.prop('disabled', true);
const deviceId = $('#device-select').val();
$.post('/enable_wireless_debugging', { device_id: deviceId }, function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: '无线调试已启用。',
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '启用无线调试失败。',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
enableWirelessDebuggingButton.prop('disabled', false);
});
}
// 禁用无线调试
function disableWirelessDebugging() {
const disableWirelessDebuggingButton = $('#disable-wireless-debugging-button');
disableWirelessDebuggingButton.prop('disabled', true);
const deviceId = $('#device-select').val();
$.post('/disable_wireless_debugging', { device_id: deviceId }, function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: '无线调试已禁用。',
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '禁用无线调试失败。',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
disableWirelessDebuggingButton.prop('disabled', false);
});
}
// 启用投屏互动模式
function startScrcpy() {
const startScrcpyButton = $('#start-scrcpy-button');
startScrcpyButton.prop('disabled', true);
const deviceId = $('#device-select').val();
if (!deviceId) {
Swal.fire({
icon: 'warning',
title: '请选择设备。',
confirmButtonText: '确定'
});
startScrcpyButton.prop('disabled', false);
return;
}
$.post('/scrcpy', { device_id: deviceId }, function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'scrcpy 已启动。',
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '启动 scrcpy 失败',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
startScrcpyButton.prop('disabled', false);
});
}
// 停用投屏互动模式
function stopScrcpy() {
const stopScrcpyButton = $('#stop-scrcpy-button');
stopScrcpyButton.prop('disabled', true);
$.post('/stop_scrcpy', function(data) {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'scrcpy 已停止。',
confirmButtonText: '确定'
});
} else {
Swal.fire({
icon: 'error',
title: '停止 scrcpy 失败',
text: data.error,
confirmButtonText: '确定'
});
}
}).always(function() {
stopScrcpyButton.prop('disabled', false);
});
}
7、style.css代码
定义Web应用的样式,用于提升界面的美观度。
/* 页面整体样式 */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
background-color: #f8f9fa;
padding: 20px;
}
/* 标题样式 */
h1 {
text-align: center;
color: #343a40;
}
/* 卡片标题居中 */
.card-title {
text-align: center;
}
/* 卡片样式 */
.card {
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* 卡片标题 */
.card-header h2 {
margin-bottom: 0;
font-size: 1.5rem;
}
/* 按钮样式 */
button {
width: 100%;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
transition: background-color 0.3s ease;
}
button:hover {
opacity: 0.9;
}
button:active {
transform: scale(0.98);
}
/* 表单选择器样式 */
.form-select {
margin-bottom: 10px;
}
/* 边框样式 */
.border {
border: 1px solid #ced4da;
padding: 10px;
border-radius: 4px;
}
8、Run.bat代码
@echo off
rem 获取当前脚本所在路劲
set script_path=%~dp0
rem 从相对路径启动应用
start python .\app.py
9、Pconfigure
必要的依赖库文件
| `--scrcpy-win64-v2.4 # scrcpy-64位代码文件
| `--scrcpy-win32-v2.4 # scrcpy-32位代码文件
相关参考来源:Scrcpy
五、使用场景
IT研发:开发者可以使用此工具进行Android应用的测试和调试,提高开发效率。
技术支持:技术支持人员可以远程帮助用户解决设备问题,提升服务质量。
个人用户:普通用户可以使用这个工具来管理自己的Android设备,无需复杂的命令行操作。
六、结语
更多功能仍在完善中,感谢您的耐心阅读,谢谢。