基于Python+Flask完成Android测试工具开发

一、项目背景:

随着移动互联网的快速发展,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设备,无需复杂的命令行操作。

六、结语

更多功能仍在完善中,感谢您的耐心阅读,谢谢。

不多说废话,看题目,本教程适合练手,会python+android基础的人群,文件较大,上传乃是下载链接,下面上目录: 1-1 课程导学 2-1 如何学好Android App性能测试? 2-10 详解【电量】监控值的获取方法 \' D, l" p) d6 d. K9 [7 p 2-11 详解【电量】监控脚本实现和数据分析 O, e4 X& K0 S% h8 v8 V9 ? 2-12 详解【内存】监控值的获取方法 k! e6 e# C" K% z9 k- l 2-13 详解【内存】监控脚本实现和数据分析0 d; e- S% G6 r3 H: g 2-14 详解【FPS&过度渲染】的概念和监控方法 - 分析页面卡慢的方法# G! _2 O9 T* j" K s3 v6 C0 l 2-2 工欲善其事必先利其器-性能测试环境准备 2-3 详解【启动时间】监控值的获取方法0 n( p* l; g C 2-4 详解【启动时间】监控脚本实现% B2 z( C( E& S: n r1 e 2-5 详解【启动时间】数据分析 2-6 【启动时间】时间戳差值监控方法概要介绍 2-7 详解【CPU】监控值的获取方法、脚本实现和数据分析 2-8 详解【流量】监控值的获取方法7 r7 ~/ D5 |+ h9 m9 i6 p) b: Y 2-9 详解【流量】监控脚本实现和数据分析2 [9 {# {$ c9 k/ T, `/ t" \ 3-1 为什么需要使用框架实现自动化测试? 3-10 UnitTest框架之TestCase,TestSuite,TestRunner简介3 A2 {1 F2 @; K 3-11 UnitTest框架之TestSuite,TestRunner自动化测试 3-12 数据驱动框架DDT简介 3-13 数据驱动框架DDT的使用方法 3-14 数据驱动框架DDT实战; 3-2 准备一个被测APP 3-3 工欲善其事必先利其器-自动化测试环境准备 3-4 Android App自动化测试(一) 3-5 Android App自动化测试(二) 3-6 Android App自动化测试(三) 3-7 UnitTest框架之TestFixture简介 3-8 UnitTest框架之TestFixture自动化测试(一) 3-9 UnitTest框架之TestFixture自动化测试(二) 4-1 如何学好Android App API接口测试? 4-10 Fiddler构造HTTP Get请求 4-11 Fiddler构造HTTP Post请求 4-12 Fiddler抓取手机上的网络数据包 4-13 为什么使用PostMan做API接口测试 4-14 工欲善其事必先利其器-PostMan工具准备 4-15 PostMan测试HTTP Get请求 4-16 PostMan测试HTTP Post请求 4-17 数据驱动DDT实现API接口自动化测试简介) 4-18 Python requests测试HTTP中的Get、Post请求 4-19 数据驱动DDT实现API接口自动化测试(一) 4-2 什么是API 4-20 数据驱动DDT实现API接口自动化测试(二); 4-3 抓包神器Fiddler简介 4-4 Fiddler抓包原理解析 4-5 Fiddler修改客户端发出的请求(一) 4-6 Fiddler修改客户端发出的请求(二) 4-7 Fiddler修改服务器端返回的内容 4-8 Fiddler实现会话的过滤、对比及请求的编解码 4-9 Fiddler实现Host的配置 5-1 测试工程师为什么需要掌握持续集成? 5-2 持续集成的概念、流程和意义 5-3 讲解持续集成工具Jenkins3 5-4 Jenkins工具密码的修改 5-5 Jenkins工具的配置说明 5-6 Jenkins工具系统配置和Job配置 5-7 Jenkins工具手动持续集成实战 5-8 Jenkins工具自动化持续集成实战 6-1 Native App自动化测试及Appuim框架介绍 6-2 自动化测试环境、元素识别工具、脚本设计原则-LOVE原则的讲解 6-3 Native App自动化脚本的实现 6-4 Appium自动化测试框架API讲解与案例实践(一) 6-5 Appium自动化测试框架API讲解与案例实践(二) 6-6 Appium自动化测试框架API讲解与案例实践(三) 6-7 Appium自动化测试框架API讲解与案例实践(四) 6-8 Appium自动化测试框架API讲解与案例实践(五) 6-9 Appium自动化测试框架API讲解与案例实践(六) 7-1 Hybrid App自动化测试概要 7-2 Appium基于Sele
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值