Alisson Machado
07 March 2017

Python + ADB

Recentemente fiz um projeto para efetuar testes em dispositivos android utilizando a linguagem python. Esses testes eram feitos em um celular da samsung conectados via usb a um computador com windows e os dispositivos era testados utilizando um comando chamado adb (Android Debug Bridge). Todos os meus outros posts foram feitos utilizando Linux, esse eu vou fazer utilizando o windows pois quero replicar o mesmo ambiente do cliente e mostar que o python também roda muito bem em ambientes windows. O primeiro passo é instalar um cara chamado android studio, pois nele já tem o sdkmanager o adb e todos os requisitos necessários para o desenvolvimento android. Para instalar é só baixar no site oficial: https://developer.android.com/studio/install.html Após fazer a instalação eu coloquei o caminho do adb dentro da variavel PATH do windows. C:\Users\alisson\AppData\Local\Android\sdk\platform-tools Não manjo nada de windows, mas no windows 10 é só abrir o iniciar e digitar variáveis e ele vai aparecer uma opção chamada: Editar variáveis de ambiente do sistema. E lá vai ter a PATH e é só adicionar esse caminho lá, lembrando que tem que mudar o nome do usuário. Feito isso abra o cmd do windows e digite o comando:
C:\Users\alisson>adb devices

List of devices attached                                                                                                                                                                                                                                                                                                                                                                                 

A saída será a de cima. Então conecte o seu celular no computador e no gerenciamento de conexão USB marque a opção Depuração USB. Desconecte o USB e conecte de novo e no seu celular irá aparecer uma mensagem para você aceitar a chave do computador. Aceite e o celular já irá aparecer na lista de devices.
C:\Users\alisson>adb devices

List of devices attached

4012e40b        device

O meu celular é esse 4012e40b. Caso eu queira entrar dentro do dispositivo é só digitar o seguinte comando:
C:\Users\alisson>adb shell
shell@A6020l36:/ $

eai já era, você está dentro do sistema android, pra quem conhece linux muitos dos comandos vão funcionar, como o ls, cd, pwd e entre outros comandos básicos. Pra voltar ao cmd é só pressionar CTRL+D Bom isso é o básico que é necessário saber por enquanto, pois isso permite saber que o seu celular está corretamente configurado para que sejam efetuados os testes. Nos testes que foram feitos na empresa, utilizamos basicamente 3 módulos do python, sendo eles:
  • subprocess ( Executa comandos do sistema )
  • ElementTree ( Modulo para trabalhar com XML )
  • threading ( Modulo de threads para executar testes em paralelo )
No exemplo abaixo não vou usar threads, mas caso vocês precisem eu tenho um post especifico falando de threads neste link ( Threads em Python ). O script completo é esse:
import subprocess
import xml.etree.ElementTree as ET
import time


def get_devices():
    output = subprocess.Popen('adb devices', shell=True, stdout=subprocess.PIPE).communicate()[0]
    output = str(output).split('attached')[1]
    output = output.split('device')[0]
    output = output.replace("\\n", "").replace("\\r", "")
    output = output.replace("\\t", "")
    return output

def get_screen(serial):
    output = subprocess.Popen('adb -s %s shell uiautomator dump'%serial,shell=True,stdout=subprocess.PIPE).communicate()[0]
    print(output)
    output = subprocess.Popen('adb -s %s pull /storage/sdcard0/window_dump.xml'%serial,shell=True,stdout=subprocess.PIPE).communicate()[0]
    print(output)


def tap_screen(app_name,x,y):
    print("Abrindo %s"%app_name)
    output = subprocess.Popen("adb shell input tap %s %s"%(x,y),shell=True,stdout=subprocess.PIPE)
    time.sleep(5)
    print("Voltando a tela anterior")
    output = subprocess.Popen("adb shell input keyevent 4",shell=True,stdout=subprocess.PIPE)

def test_app(node,app_name):
    if node.attrib.get('text') == app_name:            
        position = node.attrib.get('bounds').split(']')[0]
        position = position.replace("[","").split(",")
        print("Testando APP")
        tap_screen(app_name, position[0], position[1])
    else:   
        for n in node.findall('node'):                
            test_app(n,app_name)

def get_apps():
    xml = ET.parse('window_dump.xml')
    root = xml.getroot()
    app = test_app(root,'Play Store')    



device = get_devices()
get_screen(device)
get_apps()

Agora explicando o que isso faz. A função get_devices abaixo:
def get_devices():
    output = subprocess.Popen('adb devices', shell=True, stdout=subprocess.PIPE).communicate()[0]
    output = str(output).split('attached')[1]
    output = output.split('device')[0]
    output = output.replace("\\n", "").replace("\\r", "")
    output = output.replace("\\t", "")
    return output
Ela executa um comando dentro do sistema operacional, o comando adb devices vai listar todos os seus dispositivos conectados, então a saída é armazenada dentro da variavel output e na sequencia vão sendo aplicados alguns comandos para remover caracteres especiais como quebra de linha e tabs para que no final venha só o SERIAL do dispositivo conectado. Uma vez que tenho esse serial, é possível pegar um dump da tela para saber a posição dos icones dentro dela. A tela que eu fiz um dump foi a seguinte: O script criado vai procurar o botão da play store, clicar nele e depois voltar para essa mesma tela. Para que isso aconteça foi criada a função get_screen.
def get_screen(serial):
    output = subprocess.Popen('adb -s %s shell uiautomator dump'%serial,shell=True,stdout=subprocess.PIPE).communicate()[0]
    print(output)
    output = subprocess.Popen('adb -s %s pull /storage/sdcard0/window_dump.xml'%serial,shell=True,stdout=subprocess.PIPE).communicate()[0]
    print(output)
Essa função roda o comando adb shell uiatomator dump, que gera um xml com as posições do icones na tela, uma vez que esse xml foi gerado ele é copiado para o diretorio onde está sendo executado o script com o comando adb pull. Logo na sequencia é executada a função get_apps().
def get_apps():
    xml = ET.parse('window_dump.xml')
    root = xml.getroot()
    app = test_app(root,'Play Store')   
Essa função basicamente lê o arquivo xml gerado pelo adb faz o parse dele de string para xml e manda para a função test_app.
def test_app(node,app_name):
    if node.attrib.get('text') == app_name:            
        position = node.attrib.get('bounds').split(']')[0]
        position = position.replace("[","").split(",")
        print("Testando APP")
        tap_screen(app_name, position[0], position[1])
    else:   
        for n in node.findall('node'):                
            test_app(n,app_name)
Essa função verifica se o elemento atual do xml possui um atributo chamado text e se esse atributo é igual ao nome do app procurado, que no caso é Play Store, caso esse app não seja encontrado é realizado um for buscando os elementos filhos do elemento atual, fazendo assim uma recursividade até que sejam percorridos todos os elementos filhos desse xml. Quando o APP é encontrado, é pegado o valor do atributo bounds que guarda a posição do item na tela, essa posição é guarda na variável position e depois é chamada a função tap_screen que clica no botão.
def tap_screen(app_name,x,y):
    print("Abrindo %s"%app_name)
    output = subprocess.Popen("adb shell input tap %s %s"%(x,y),shell=True,stdout=subprocess.PIPE)
    time.sleep(5)
    print("Voltando a tela anterior")
    output = subprocess.Popen("adb shell input keyevent 4",shell=True,stdout=subprocess.PIPE)
Essa função recebe como parâmetros, o nome do aplicativo somente para que seja informado o nome na tela e as posições x e y do botão, essas posição são passadas para o comando adb shell input tap que vai clicar no play store, como esse app demora para abrir é aguardado o tempo de 5 segundos e depois é executado o comando adb shell input keyvent 4, que é o equivalente ao botão voltar do android, então é voltado para a tela anterior. Alguns dos eventos possíveis são: 0 --> "KEYCODE_UNKNOWN" 1 --> "KEYCODE_MENU" 2 --> "KEYCODE_SOFT_RIGHT" 3 --> "KEYCODE_HOME" 4 --> "KEYCODE_BACK" 5 --> "KEYCODE_CALL" 6 --> "KEYCODE_ENDCALL" 7 --> "KEYCODE_0" 8 --> "KEYCODE_1" 9 --> "KEYCODE_2" 10 --> "KEYCODE_3" 11 --> "KEYCODE_4" 12 --> "KEYCODE_5" 13 --> "KEYCODE_6" 14 --> "KEYCODE_7" 15 --> "KEYCODE_8" 16 --> "KEYCODE_9" 17 --> "KEYCODE_STAR" 18 --> "KEYCODE_POUND" 19 --> "KEYCODE_DPAD_UP" 20 --> "KEYCODE_DPAD_DOWN" 21 --> "KEYCODE_DPAD_LEFT" 22 --> "KEYCODE_DPAD_RIGHT" 23 --> "KEYCODE_DPAD_CENTER" 24 --> "KEYCODE_VOLUME_UP" 25 --> "KEYCODE_VOLUME_DOWN" 26 --> "KEYCODE_POWER" 27 --> "KEYCODE_CAMERA" 28 --> "KEYCODE_CLEAR" 29 --> "KEYCODE_A" 30 --> "KEYCODE_B" 31 --> "KEYCODE_C" 32 --> "KEYCODE_D" 33 --> "KEYCODE_E" 34 --> "KEYCODE_F" 35 --> "KEYCODE_G" Basicamente é isso ai, qualquer dúvida é só em dar um salve.