在 Android 中使用 UIAutomator 執行自動化任務

63845153 9年前發布 | 16K 次閱讀 安卓開發 Android開發 移動開發

用代碼來替代任何重復性的工作,一直是我的追求。這周,我又寫了一段腳本,讓我從無盡的重復工作中解脫了出來。

問題 & 需求

最近在修 bug 的時候,為了達到一個合適的測試環境,我需要一直要重復執行這些操作:

  • 完全關閉 App
  • 打開應用設置頁清空 App 數據
  • 在權限界面打開「存儲空間」權限
  • 啟動 App,登錄帳號

主要過程就是不斷地點擊屏幕,等待界面切換,輸入內容。我就想讓代碼來幫忙點擊這些固定的位置,然后輸入預設的內容,自動化完成這些無聊的操作。

整理一下,需要實現的功能大概就是:

  • 根據文本信息確定點擊位置
  • 執行點擊操作
  • 執行輸入操作
  • 啟動一些界面

解決方案

要用命令來控制 Android 設備,那肯定是選用 adb 了。GitHub 上有個叫 awesome-adb 的項目,列舉了 adb 的各種用法,其中有提到 調起 Activity 和 模擬按鍵輸入 的操作。另外查閱資料得知 uiautomator 命令可以獲取屏幕中的控件信息,從中可以提取到控件的位置,用于模擬點擊。下面用 Python 來實現整個流程。

這里先提一下兩個工具函數,方便后續的代碼展示。一個是用來執行 adb 的,另外一個是裝飾器,在目標函數執行完之后休眠一會,等待 UI 的響應。

def run(cmd):
    """執行 adb 命令"""
    # adb <CMD>
    return subprocess.check_output(('adb %s' % cmd).split(' '))


def sleep_later(duration=0.5):
    """裝飾器:在函數執行完成之后休眠等待一段時間"""
    def wrapper(func):
        def do(*args, **kwargs):
            func(*args, **kwargs)
            if 'duration' in kwargs.keys():
                time.sleep(kwargs['duration'])
            else:
                time.sleep(duration)

        return do
    return wrapper

根據文本信息點擊屏幕

需要先用 uiautomator 命令來獲取屏幕信息。

dump_file = '/sdcard/window_dump.xml'

def dump_layout():
    print 'Dump window layouts'
    # adb shell uiautomator dump <FILE>
    run('shell uiautomator dump %s' % dump_file)

得到的 XML 文件是由 node 節點組成的,其中的 text 和 bounds 屬性是我們需要的。可以根據文本去匹配到相應的 node 節點,然后解析出控件的邊界信息,后續只要在這個邊界內點擊就可以模擬真實的操作了。

<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
    <node
        index="0"
        text=""
        resource-id=""
        class="android.widget.FrameLayout"
        package="com.teslacoilsw.launcher"
        content-desc=""
        checkable="false"
        checked="false"
        clickable="false"
        enabled="true"
        focusable="false"
        focused="false"
        scrollable="false"
        long-clickable="false"
        password="false"
        selected="false"
        bounds="[0,0][1080,1920]">

        <!-- many nodes -->

    </node>
</hierarchy>

這里先用了 cat 命令直接讀出 XML 的內容,然后用 lxml 解析匹配目標節點。后面用正則表達式提取出邊界的坐標點,然后直接計算出邊界矩形的中心點。

def parse_bounds(text):
    # adb shell cat /sdcard/window_dump.xml
    dumps = run('shell cat %s' % dump_file)
    nodes = etree.XML(dumps)
    return nodes.xpath(u'//node[@text="%s"]/@bounds' % (text))[0]

bounds_pattern = re.compile(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]')

def point_in_bounds(bounds):
    """
    '[42,1023][126,1080]'
    """
    points = bounds_pattern.match(bounds).groups()
    points = map(int, points)
    return (points[0] + points[2]) / 2, (points[1] + points[3]) / 2

再用 input 命令,結合上面的幾個函數,可以完成這個需求了。

@sleep_later()
def click_with_keyword(keyword, dump=True, **kwargs):
    # 有的屏幕需要多次點擊時,dump 可以設置為 False,使用上一次的屏幕數據
    if dump:
        dump_layout()
    bounds = parse_bounds(keyword)
    point = point_in_bounds(bounds)

    print 'Click "%s" (%d, %d)' % (keyword, point[0], point[1])
    # adb shell input tap <x> <y>
    run('shell input tap %d %d' % point)

模擬輸入

這個比較簡單,直接使用 input text 命令。另外還實現了模擬按返回鍵。

@sleep_later()
def keyboard_input(text):
    # adb shell input text <string>
    run('shell input text %s' % text)


@sleep_later()
def keyboard_back():
    # adb shell input keyevent 4
    run('shell input keyevent 4')

停止應用、清除數據、啟動 Activity

這一些命令操作,按照 awesome-adb 的文檔執行就好。

@sleep_later()
def force_stop(package):
    print 'Force stop %s' % package
    # adb shell am force-stop <package>
    run('shell am force-stop %s' % package)


@sleep_later(0.5)
def start_activity(activity):
    print 'Start activity %s' % activity
    # adb shell am start -n <activity>
    run('shell am start -n %s' % activity)


@sleep_later(0.5)
def clear_data(package):
    print 'Clear app data: %s' % package
    # adb shell pm clear <package>
    run('shell pm clear %s' % package)

另外,打開指定應用的設置界面,需要指定 ACTION 和 DATA。

@sleep_later()
def open_app_detail(package):
    print 'Open application detail setting: %s' % package
    # adb shell am start -a ACTION -d DATA
    intent_action = 'android.settings.APPLICATION_DETAILS_SETTINGS'
    intent_data = 'package:%s' % package

    run('shell am start -a %s -d %s' % (intent_action, intent_data))

拼裝整個流程

target_package = 'com.mingdao'
launcher_activity = 'com.mingdao/.presentation.ui.login.WelcomeActivity'

def main():
    username, password = sys.argv[1:3]
    # 停止應用
    force_stop(target_package)
    # 清除數據
    clear_data(target_package)
    # 啟動應用設置頁
    open_app_detail(target_package)
    # 進入權限頁
    click_with_keyword(u'權限')
    # 打開「存儲控件權限」
    click_with_keyword(u'存儲空間')
    # 按一下返回
    keyboard_back()
    # 啟動 app
    start_activity(launcher_activity)
    # 歡迎頁跳過
    click_with_keyword(u'跳過')
    # 選中「帳號」輸入框
    click_with_keyword(u'手機或郵箱', duration=0)
    # 輸入帳號
    keyboard_input(username)
    # 選中「密碼」輸入框
    click_with_keyword(u'密碼', dump=False, duration=0)
    # 輸入密碼
    keyboard_input(password)
    # 點一下登錄按鈕
    click_with_keyword(u'登錄', dump=False)

前面把各種操作寫好,主流程就很清晰啦,照著手動操作的過程,一步一步調函數就好了。

效果圖

Reference

 

來自:http://brucezz.itscoder.com/articles/2016/12/05/use-uiautomator-in-android/

 

 本文由用戶 63845153 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!