__init__.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. ::Timeout
  5. atx-agent:ReverseProxy use http.DefaultTransport. Default Timeout: 30s
  6. |-- Dial --|-- TLS handshake --|-- Request --|-- Resp.headers --|-- Respose.body --|
  7. |------------------------------ http.Client.Timeout -------------------------------|
  8. Refs:
  9. - https://golang.org/pkg/net/http/#RoundTripper
  10. - http://colobu.com/2016/07/01/the-complete-guide-to-golang-net-http-timeouts
  11. """
  12. from __future__ import absolute_import, print_function
  13. import functools
  14. import hashlib
  15. import io
  16. import json
  17. import os
  18. import re
  19. import shutil
  20. import subprocess
  21. import sys
  22. import threading
  23. import time
  24. import warnings
  25. from collections import namedtuple
  26. from datetime import datetime
  27. from subprocess import list2cmdline
  28. import humanize
  29. import progress.bar
  30. import requests
  31. import six
  32. import six.moves.urllib.parse as urlparse
  33. from retry import retry
  34. from ssat_sdk.uiautomator2 import adbutils
  35. from ssat_sdk.uiautomator2.exceptions import (GatewayError, JsonRpcError, ConnectError,
  36. NullObjectExceptionError,
  37. NullPointerExceptionError,
  38. SessionBrokenError,
  39. StaleObjectExceptionError, UiaError,
  40. UiAutomationNotConnectedError,
  41. UiObjectNotFoundError)
  42. from ssat_sdk.uiautomator2.session import Session, set_fail_prompt # noqa: F401
  43. from ssat_sdk.uiautomator2.version import __atx_agent_version__
  44. if six.PY2:
  45. FileNotFoundError = OSError
  46. DEBUG = False
  47. HTTP_TIMEOUT = 60
  48. class _ProgressBar(progress.bar.Bar):
  49. message = "progress"
  50. suffix = '%(percent)d%% [%(eta_td)s, %(speed)s]'
  51. @property
  52. def speed(self):
  53. return humanize.naturalsize(
  54. self.elapsed and self.index / self.elapsed, gnu=True) + '/s'
  55. def log_print(s):
  56. thread_name = threading.current_thread().getName()
  57. print(thread_name + ": " + datetime.now().strftime('%H:%M:%S,%f')[:-3] +
  58. " " + s)
  59. def _is_wifi_addr(addr):
  60. if not addr:
  61. return False
  62. if re.match(r"^https?://", addr):
  63. return True
  64. m = re.search(r"(\d+\.\d+\.\d+\.\d+)", addr)
  65. if m and m.group(1) != "127.0.0.1":
  66. return True
  67. return False
  68. def connect(addr=None):
  69. """
  70. Args:
  71. addr (str): uiautomator server address or serial number. default from env-var ANDROID_DEVICE_IP
  72. Returns:
  73. UIAutomatorServer
  74. Raises:
  75. ConnectError
  76. Example:
  77. connect("10.0.0.1:7912")
  78. connect("10.0.0.1") # use default 7912 port
  79. connect("http://10.0.0.1")
  80. connect("http://10.0.0.1:7912")
  81. connect("cff1123ea") # adb device serial number
  82. """
  83. if not addr or addr == '+':
  84. addr = os.getenv('ANDROID_DEVICE_IP')
  85. if _is_wifi_addr(addr):
  86. return connect_wifi(addr)
  87. return connect_usb(addr)
  88. def connect_adb_wifi(addr):
  89. """
  90. Run adb connect, and then call connect_usb(..)
  91. Args:
  92. addr: ip+port which can be used for "adb connect" argument
  93. Raises:
  94. ConnectError
  95. """
  96. assert isinstance(addr, six.string_types)
  97. subprocess.call([adbutils.adb_path(), "connect", addr])
  98. try:
  99. subprocess.call([adbutils.adb_path(), "-s", addr, "wait-for-device"], timeout=2)
  100. except subprocess.TimeoutExpired:
  101. raise ConnectError("Fail execute", "adb connect " + addr)
  102. return connect_usb(addr)
  103. def connect_usb(serial=None, healthcheck=True):
  104. print("uiautomator2.__init.connect_usb: serial=",serial,";healthcheck=",healthcheck)
  105. """
  106. Args:
  107. serial (str): android device serial
  108. healthcheck (bool): start uiautomator if not ready
  109. Returns:
  110. UIAutomatorServer
  111. Raises:
  112. ConnectError
  113. """
  114. adb = adbutils.AdbClient()
  115. if not serial:
  116. device = adb.must_one_device()
  117. print("uiautomator2.__init.connect_usb: not serial, device=", device)
  118. else:
  119. device = adbutils.AdbDevice(adb, serial)
  120. # adb = adbutils.Adb(serial)
  121. global connect_serial
  122. connect_serial = device.serial
  123. log_print("connect_serial:%s" % str(connect_serial))
  124. lport = device.forward_port(7912)
  125. print("uiautomator2.__init.connect_usb: lport=", lport)
  126. d = connect_wifi('127.0.0.1:' + str(lport))
  127. print("uiautomator2.__init.connect_usb: UIAutomatorServer=", d)
  128. if healthcheck:
  129. if not d.agent_alive:
  130. warnings.warn("backend atx-agent is not alive, start again ...",
  131. RuntimeWarning)
  132. # TODO: /data/local/tmp might not be execuable and atx-agent can be somewhere else
  133. device.shell_output("/data/local/tmp/atx-agent", "server", "-d")
  134. deadline = time.time() + 3
  135. while time.time() < deadline:
  136. if d.alive:
  137. break
  138. elif not d.alive:
  139. warnings.warn("backend uiautomator2 is not alive, start again ...",
  140. RuntimeWarning)
  141. d.reset_uiautomator()
  142. print("uiautomator2.__init.connect_usb: atx-agent is alive and uiautomator app is alive!")
  143. return d
  144. def connect_wifi(addr=None):
  145. """
  146. Args:
  147. addr (str) uiautomator server address.
  148. Returns:
  149. UIAutomatorServer
  150. Raises:
  151. ConnectError
  152. Examples:
  153. connect_wifi("10.0.0.1")
  154. """
  155. print("uiautomator2.__init.connect_wifi: start")
  156. if '://' not in addr:
  157. addr = 'http://' + addr
  158. if addr.startswith('http://'):
  159. u = urlparse.urlparse(addr)
  160. host = u.hostname
  161. port = u.port or 7912
  162. return UIAutomatorServer(host, port)
  163. else:
  164. raise ConnectError("address should start with http://")
  165. connect_serial = None
  166. def check_adb_connect():
  167. if not connect_serial:
  168. return True
  169. CMD_ADB_CHECK = "adb -s %s shell getprop ro.build.version.sdk" % connect_serial
  170. p = subprocess.Popen(CMD_ADB_CHECK, bufsize=128, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  171. outInfo = p.stdout.read()
  172. # log_print(u"查询SDK版本号outInfo:%s" % str(outInfo))
  173. errInfo = p.stderr.read()
  174. p.kill()
  175. if errInfo.__len__() > 0:
  176. log_print(u"查询SDK版本号失败,出错信息:%s" % str(errInfo))
  177. return False
  178. else:
  179. return True
  180. def check_adb_connect_type():
  181. CMD_ADB_DEVICES = "adb devices"
  182. p = subprocess.Popen(CMD_ADB_DEVICES, bufsize=128, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  183. outInfo = p.stdout.read()
  184. if "192.168" in outInfo:
  185. return "tcp/ip"
  186. else:
  187. return "usb"
  188. def disconnectAdb():
  189. if not connect_serial:
  190. return True
  191. # CMD_ADB_DISCONNECT = "adb disconnect %s" % connect_serial
  192. CMD_ADB_DISCONNECT = "adb disconnect %s" % str(connect_serial)
  193. p = subprocess.Popen(CMD_ADB_DISCONNECT, bufsize=128, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  194. p.kill()
  195. def connectAdb():
  196. if not connect_serial:
  197. return True
  198. CMD_ADB_CONNECT = "adb connect %s" % connect_serial
  199. p = subprocess.Popen(CMD_ADB_CONNECT, bufsize=128, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  200. log_print(u"执行adb重连机制CMD_ADB_CONNECT:%s" % str(CMD_ADB_CONNECT))
  201. p.kill()
  202. def killAdb():
  203. if not connect_serial:
  204. return True
  205. CMD_ADB_KILL_Server = "adb kill-server"
  206. p = subprocess.Popen(CMD_ADB_KILL_Server, bufsize=128, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  207. p.kill()
  208. def confirmAdb(serial=None):
  209. # 如果传来的serial不为空,则重新连接传入的serial对应的设备
  210. if serial:
  211. global connect_serial
  212. connect_serial = serial
  213. if not connect_serial:
  214. # log_print("序列为空返回!")
  215. return True
  216. # ret = check_adb_connect()
  217. if check_adb_connect():
  218. # log_print("adb已经连接connect_serial:%s" % str(connect_serial))
  219. return True
  220. else:
  221. log_print(u"connect_serial:%s,Adb 未连接,尝试重连!!!" % str(connect_serial))
  222. count = 0
  223. while True:
  224. if count > 30:
  225. return False
  226. break
  227. # print(u"adb重连第count:%s次" % str(count))
  228. log_print(u"adb重连第count:%s次" % str(count))
  229. disconnectAdb()
  230. time.sleep(5)
  231. if count > 1:
  232. log_print(u"adb重连大于2次,仍未连接成功,kill adb server")
  233. killAdb()
  234. time.sleep(5)
  235. # disconnectAdb()
  236. # time.sleep(5)
  237. if check_adb_connect():
  238. # print(u"adb重连成功count:%s" % str(count))
  239. log_print(u"adb重连成功count:%s" % str(count))
  240. # printLog(u"adb重连成功count:%s"%str(count))
  241. # 重连之后要重新构建连接
  242. return True
  243. connectAdb()
  244. time.sleep(5)
  245. if check_adb_connect():
  246. # print(u"adb重连成功count:%s" % str(count))
  247. log_print(u"adb重连成功count:%s" % str(count))
  248. # printLog(u"adb重连成功count:%s"%str(count))
  249. # 重连之后要重新构建连接
  250. return True
  251. break
  252. else:
  253. log_print(u"adb重连失败count:%s" % str(count))
  254. count = count + 1
  255. time.sleep(1)
  256. class TimeoutRequestsSession(requests.Session):
  257. def __init__(self):
  258. super(TimeoutRequestsSession, self).__init__()
  259. # refs: https://stackoverflow.com/questions/33895739/python-requests-cant-load-any-url-remote-end-closed-connection-without-respo
  260. adapter = requests.adapters.HTTPAdapter(max_retries=3)
  261. self.mount("http://", adapter)
  262. self.mount("https://", adapter)
  263. def request(self, method, url, **kwargs):
  264. if kwargs.get('timeout') is None:
  265. kwargs['timeout'] = HTTP_TIMEOUT
  266. verbose = hasattr(self, 'debug') and self.debug
  267. if verbose:
  268. data = kwargs.get('data') or '""'
  269. if isinstance(data, dict):
  270. data = json.dumps(data)
  271. time_start = time.time()
  272. print(
  273. datetime.now().strftime("%H:%M:%S.%f")[:-3],
  274. "$ curl -X {method} -d '{data}' '{url}'".format(
  275. method=method, url=url, data=data))
  276. try:
  277. resp = super(TimeoutRequestsSession, self).request(
  278. method, url, **kwargs)
  279. except requests.ConnectionError:
  280. raise EnvironmentError(
  281. "atx-agent is not running. Fix it with following steps.\n1. Plugin device into computer.\n2. Run command \"python -m uiautomator2 init\""
  282. )
  283. else:
  284. if verbose:
  285. print(
  286. datetime.now().strftime("%H:%M:%S.%f")[:-3],
  287. "Response (%d ms) >>>\n" % (
  288. (time.time() - time_start) * 1000) +
  289. resp.text.rstrip() + "\n<<< END")
  290. return resp
  291. def plugin_register(name, plugin, *args, **kwargs):
  292. """
  293. Add plugin into UIAutomatorServer
  294. Args:
  295. name: string
  296. plugin: class or function which take d as first parameter
  297. Example:
  298. def upload_screenshot(d):
  299. def inner():
  300. d.screenshot("tmp.jpg")
  301. # use requests.post upload tmp.jpg
  302. return inner
  303. plugin_register("upload_screenshot", save_screenshot)
  304. d = u2.connect()
  305. d.ext_upload_screenshot()
  306. """
  307. UIAutomatorServer.plugins()[name] = (plugin, args, kwargs)
  308. def plugin_clear():
  309. UIAutomatorServer.plugins().clear()
  310. class UIAutomatorServer(object):
  311. __isfrozen = False
  312. __plugins = {}
  313. def __init__(self, host, port=7912):
  314. """
  315. Args:
  316. host (str): host address
  317. port (int): port number
  318. Raises:
  319. EnvironmentError
  320. """
  321. self._host = host
  322. self._port = port
  323. self._reqsess = TimeoutRequestsSession(
  324. ) # use requests.Session to enable HTTP Keep-Alive
  325. self._server_url = 'http://{}:{}'.format(host, port)
  326. self._server_jsonrpc_url = self._server_url + "/jsonrpc/0"
  327. self._default_session = Session(self, None)
  328. self._cached_plugins = {}
  329. self.__devinfo = None
  330. self._hooks = {}
  331. self.platform = None # hot fix for weditor
  332. self.ash = AdbShell(self.shell) # the powerful adb shell
  333. self.wait_timeout = 20.0 # wait element timeout
  334. self.click_post_delay = None # wait after each click
  335. self._freeze() # prevent creating new attrs
  336. # self._atx_agent_check()
  337. def _freeze(self):
  338. self.__isfrozen = True
  339. @staticmethod
  340. def plugins():
  341. return UIAutomatorServer.__plugins
  342. def __setattr__(self, key, value):
  343. """ Prevent creating new attributes outside __init__ """
  344. if self.__isfrozen and not hasattr(self, key):
  345. raise TypeError("Key %s does not exist in class %r" % (key, self))
  346. object.__setattr__(self, key, value)
  347. def __str__(self):
  348. return 'uiautomator2 object for %s:%d' % (self._host, self._port)
  349. def __repr__(self):
  350. return str(self)
  351. def _atx_agent_check(self):
  352. """ check atx-agent health status and version """
  353. try:
  354. version = self._reqsess.get(
  355. self.path2url('/version'), timeout=5).text
  356. if version != __atx_agent_version__:
  357. warnings.warn(
  358. 'Version dismatch, expect "%s" actually "%s"' %
  359. (__atx_agent_version__, version),
  360. Warning,
  361. stacklevel=2)
  362. # Cancel bellow code to make connect() return faster.
  363. # launch service to prevent uiautomator killed by Android system
  364. # self.adb_shell('am', 'startservice', '-n', 'com.github.uiautomator/.Service')
  365. except (requests.ConnectionError, ) as e:
  366. raise EnvironmentError(
  367. "atx-agent is not responding, need to init device first")
  368. @property
  369. def debug(self):
  370. return hasattr(self._reqsess, 'debug') and self._reqsess.debug
  371. @debug.setter
  372. def debug(self, value):
  373. self._reqsess.debug = bool(value)
  374. @property
  375. def serial(self):
  376. return self.shell(['getprop', 'ro.serialno'])[0].strip()
  377. @property
  378. def jsonrpc(self):
  379. """
  380. Make jsonrpc call easier
  381. For example:
  382. self.jsonrpc.pressKey("home")
  383. """
  384. return self.setup_jsonrpc()
  385. def path2url(self, path):
  386. return urlparse.urljoin(self._server_url, path)
  387. def window_size(self):
  388. """ return (width, height) """
  389. info = self._reqsess.get(self.path2url('/info')).json()
  390. w, h = info['display']['width'], info['display']['height']
  391. if (w > h) != (self.info["displayRotation"] % 2 == 1):
  392. w, h = h, w
  393. return w, h
  394. def hooks_register(self, func):
  395. """
  396. Args:
  397. func: should accept 3 args. func_name:string, args:tuple, kwargs:dict
  398. """
  399. self._hooks[func] = True
  400. def hooks_apply(self, stage, func_name, args=(), kwargs={}, ret=None):
  401. """
  402. Args:
  403. stage(str): one of "before" or "after"
  404. """
  405. for fn in self._hooks.keys():
  406. fn(stage, func_name, args, kwargs, ret)
  407. def setup_jsonrpc(self, jsonrpc_url=None):
  408. """
  409. Wrap jsonrpc call into object
  410. Usage example:
  411. self.setup_jsonrpc().pressKey("home")
  412. """
  413. if not jsonrpc_url:
  414. jsonrpc_url = self._server_jsonrpc_url
  415. class JSONRpcWrapper():
  416. def __init__(self, server):
  417. self.server = server
  418. self.method = None
  419. def __getattr__(self, method):
  420. self.method = method # jsonrpc function name
  421. return self
  422. def __call__(self, *args, **kwargs):
  423. http_timeout = kwargs.pop('http_timeout', HTTP_TIMEOUT)
  424. params = args if args else kwargs
  425. return self.server.jsonrpc_retry_call(jsonrpc_url, self.method,
  426. params, http_timeout)
  427. return JSONRpcWrapper(self)
  428. def jsonrpc_retry_call(self, *args,
  429. **kwargs): # method, params=[], http_timeout=60):
  430. try:
  431. return self.jsonrpc_call(*args, **kwargs)
  432. except (GatewayError, ):
  433. warnings.warn(
  434. "uiautomator2 is not reponding, restart uiautomator2 automatically",
  435. RuntimeWarning,
  436. stacklevel=1)
  437. # for XiaoMi, want to recover uiautomator2 must start app:com.github.uiautomator
  438. self.reset_uiautomator()
  439. return self.jsonrpc_call(*args, **kwargs)
  440. except UiAutomationNotConnectedError:
  441. warnings.warn(
  442. "UiAutomation not connected, restart uiautoamtor",
  443. RuntimeWarning,
  444. stacklevel=1)
  445. self.reset_uiautomator()
  446. return self.jsonrpc_call(*args, **kwargs)
  447. except (NullObjectExceptionError, NullPointerExceptionError,
  448. StaleObjectExceptionError) as e:
  449. if args[1] != 'dumpWindowHierarchy': # args[1] method
  450. warnings.warn(
  451. "uiautomator2 raise exception %s, and run code again" % e,
  452. RuntimeWarning,
  453. stacklevel=1)
  454. time.sleep(1)
  455. return self.jsonrpc_call(*args, **kwargs)
  456. def jsonrpc_call(self, jsonrpc_url, method, params=[], http_timeout=60):
  457. """ jsonrpc2 call
  458. Refs:
  459. - http://www.jsonrpc.org/specification
  460. """
  461. request_start = time.time()
  462. data = {
  463. "jsonrpc": "2.0",
  464. "id": self._jsonrpc_id(method),
  465. "method": method,
  466. "params": params,
  467. }
  468. data = json.dumps(data).encode('utf-8')
  469. res = self._reqsess.post(
  470. jsonrpc_url, # +"?m="+method, #?method is for debug
  471. headers={"Content-Type": "application/json"},
  472. timeout=http_timeout,
  473. data=data)
  474. if DEBUG:
  475. print("Shell$ curl -X POST -d '{}' {}".format(data, jsonrpc_url))
  476. print("Output> " + res.text)
  477. if res.status_code == 502:
  478. raise GatewayError(
  479. res, "gateway error, time used %.1fs" %
  480. (time.time() - request_start))
  481. if res.status_code == 410: # http status gone: session broken
  482. raise SessionBrokenError("app quit or crash", jsonrpc_url,
  483. res.text)
  484. if res.status_code != 200:
  485. raise UiaError(jsonrpc_url, data, res.status_code, res.text,
  486. "HTTP Return code is not 200", res.text)
  487. jsondata = res.json()
  488. error = jsondata.get('error')
  489. if not error:
  490. return jsondata.get('result')
  491. # error happends
  492. err = JsonRpcError(error, method)
  493. if isinstance(
  494. err.data,
  495. six.string_types) and 'UiAutomation not connected' in err.data:
  496. err.__class__ = UiAutomationNotConnectedError
  497. elif err.message:
  498. if 'uiautomator.UiObjectNotFoundException' in err.message:
  499. err.__class__ = UiObjectNotFoundError
  500. elif 'android.support.test.uiautomator.StaleObjectException' in err.message:
  501. # StaleObjectException
  502. # https://developer.android.com/reference/android/support/test/uiautomator/StaleObjectException.html
  503. # A StaleObjectException exception is thrown when a UiObject2 is used after the underlying View has been destroyed.
  504. # In this case, it is necessary to call findObject(BySelector) to obtain a new UiObject2 instance.
  505. err.__class__ = StaleObjectExceptionError
  506. elif 'java.lang.NullObjectException' in err.message:
  507. err.__class__ = NullObjectExceptionError
  508. elif 'java.lang.NullPointerException' == err.message:
  509. err.__class__ = NullPointerExceptionError
  510. raise err
  511. def _jsonrpc_id(self, method):
  512. m = hashlib.md5()
  513. m.update(("%s at %f" % (method, time.time())).encode("utf-8"))
  514. return m.hexdigest()
  515. @property
  516. def agent_alive(self):
  517. try:
  518. r = self._reqsess.get(self.path2url('/version'), timeout=2)
  519. return r.status_code == 200
  520. except:
  521. return False
  522. @property
  523. def alive(self):
  524. try:
  525. r = self._reqsess.get(self.path2url('/ping'), timeout=2)
  526. if r.status_code != 200:
  527. return False
  528. r = self._reqsess.post(
  529. self.path2url('/jsonrpc/0'),
  530. data=json.dumps({
  531. "jsonrpc": "2.0",
  532. "id": 1,
  533. "method": "deviceInfo"
  534. }),
  535. timeout=2)
  536. if r.status_code != 200:
  537. return False
  538. if r.json().get('error'):
  539. return False
  540. return True
  541. except requests.exceptions.ReadTimeout:
  542. return False
  543. except EnvironmentError:
  544. return False
  545. def service(self, name):
  546. """ Manage service start or stop
  547. Example:
  548. d.service("uiautomator").start()
  549. d.service("uiautomator").stop()
  550. """
  551. u2obj = self
  552. class _Service(object):
  553. def __init__(self, name):
  554. self.name = name
  555. # FIXME(ssx): support other service: minicap, minitouch
  556. assert name == 'uiautomator'
  557. def start(self):
  558. res = u2obj._reqsess.post(u2obj.path2url('/uiautomator'))
  559. res.raise_for_status()
  560. def stop(self):
  561. res = u2obj._reqsess.delete(u2obj.path2url('/uiautomator'))
  562. if res.status_code != 200:
  563. warnings.warn(res.text)
  564. def running(self):
  565. res = u2obj._reqsess.get(u2obj.path2url("/uiautomator"))
  566. res.raise_for_status()
  567. return res.json().get("running")
  568. return _Service(name)
  569. @property
  570. def uiautomator(self):
  571. return self.service("uiautomator")
  572. def reset_uiautomator(self):
  573. """
  574. Reset uiautomator
  575. Raises:
  576. RuntimeError
  577. """
  578. # self.open_identify()
  579. stopUrl = self.path2url('/uiautomator')
  580. print("reset_uiautomator:stopUrl=",stopUrl)
  581. self._reqsess.delete(stopUrl) # stop uiautomator keeper first
  582. # wait = not unlock # should not wait IdentifyActivity open or it will stuck sometimes
  583. # self.app_start( # may also stuck here.
  584. # 'com.github.uiautomator',
  585. # '.MainActivity',
  586. # wait=False,
  587. # stop=True)
  588. time.sleep(.5)
  589. # launch atx-agent uiautomator keeper
  590. startUrl = self.path2url('/uiautomator')
  591. print("reset_uiautomator:startUrl=",startUrl)
  592. self._reqsess.post(startUrl)
  593. # wait until uiautomator2 service working
  594. deadline = time.time() + 120.0
  595. self.shell([
  596. 'am', 'start', '-n',
  597. 'com.github.uiautomator/.MainActivity'
  598. ])
  599. time.sleep(1)
  600. while time.time() < deadline:
  601. print(
  602. time.strftime("[%Y-%m-%d %H:%M:%S]"),
  603. "uiautomator is starting ...")
  604. if self.alive:
  605. # keyevent BACK if current is com.github.uiautomator
  606. # XiaoMi uiautomator will kill the app(com.github.uiautomator) when launch
  607. # it is better to start a service to make uiautomator live longer
  608. if self.current_app()['package'] != 'com.github.uiautomator':
  609. self.shell([
  610. 'am', 'startservice', '-n',
  611. 'com.github.uiautomator/.Service'
  612. ])
  613. time.sleep(1.5)
  614. else:
  615. time.sleep(.5)
  616. self.shell(['input', 'keyevent', 'BACK'])
  617. print("uiautomator back to normal")
  618. return True
  619. time.sleep(1)
  620. self.shell(['input', 'keyevent', 'KEYCODE_VOLUME_UP'])
  621. self.shell(['input', 'keyevent', 'KEYCODE_VOLUME_DOWN'])
  622. raise RuntimeError(
  623. "Uiautomator started failed. Find solutions in https://github.com/openatx/uiautomator2/wiki/Common-issues"
  624. )
  625. def healthcheck(self):
  626. """
  627. Reset device into health state
  628. Raises:
  629. RuntimeError
  630. """
  631. sh = self.ash
  632. if not sh.is_screen_on():
  633. print(time.strftime("[%Y-%m-%d %H:%M:%S]"), "wakeup screen")
  634. sh.keyevent("WAKEUP")
  635. sh.keyevent("HOME")
  636. sh.swipe(0.1, 0.9, 0.9, 0.1) # swipe to unlock
  637. sh.keyevent("HOME")
  638. sh.keyevent("BACK")
  639. self.reset_uiautomator()
  640. def app_install(self, url, installing_callback=None, server=None):
  641. """
  642. {u'message': u'downloading', "progress": {u'totalSize': 407992690, u'copiedSize': 49152}}
  643. Returns:
  644. packageName
  645. Raises:
  646. RuntimeError
  647. """
  648. r = self._reqsess.post(self.path2url('/install'), data={'url': url})
  649. if r.status_code != 200:
  650. raise RuntimeError("app install error:", r.text)
  651. id = r.text.strip()
  652. print(time.strftime('%H:%M:%S'), "id:", id)
  653. return self._wait_install_finished(id, installing_callback)
  654. def _wait_install_finished(self, id, installing_callback):
  655. bar = None
  656. downloaded = True
  657. while True:
  658. resp = self._reqsess.get(self.path2url('/install/' + id))
  659. resp.raise_for_status()
  660. jdata = resp.json()
  661. message = jdata['message']
  662. pg = jdata.get('progress')
  663. def notty_print_progress(pg):
  664. written = pg['copiedSize']
  665. total = pg['totalSize']
  666. print(
  667. time.strftime('%H:%M:%S'), 'downloading %.1f%% [%s/%s]' %
  668. (100.0 * written / total,
  669. humanize.naturalsize(written, gnu=True),
  670. humanize.naturalsize(total, gnu=True)))
  671. if message == 'downloading':
  672. downloaded = False
  673. if pg: # if there is a progress
  674. if hasattr(sys.stdout, 'isatty'):
  675. if sys.stdout.isatty():
  676. if not bar:
  677. bar = _ProgressBar(
  678. time.strftime('%H:%M:%S') + ' downloading',
  679. max=pg['totalSize'])
  680. written = pg['copiedSize']
  681. bar.next(written - bar.index)
  682. else:
  683. notty_print_progress(pg)
  684. else:
  685. pass
  686. else:
  687. print(time.strftime('%H:%M:%S'), "download initialing")
  688. else:
  689. if not downloaded:
  690. downloaded = True
  691. if bar: # bar only set in atty
  692. bar.next(pg['copiedSize'] - bar.index) if pg else None
  693. bar.finish()
  694. else:
  695. print(time.strftime('%H:%M:%S'), "download 100%")
  696. print(time.strftime('%H:%M:%S'), message)
  697. if message == 'installing':
  698. if callable(installing_callback):
  699. installing_callback(self)
  700. if message == 'success installed':
  701. return jdata.get('packageName')
  702. if jdata.get('error'):
  703. raise RuntimeError("error", jdata.get('error'))
  704. try:
  705. time.sleep(1)
  706. except KeyboardInterrupt:
  707. bar.finish() if bar else None
  708. print("keyboard interrupt catched, cancel install id", id)
  709. self._reqsess.delete(self.path2url('/install/' + id))
  710. raise
  711. def shell(self, cmdargs, stream=False, timeout=60):
  712. """
  713. Run adb shell command with arguments and return its output. Require atx-agent >=0.3.3
  714. Args:
  715. cmdargs: str or list, example: "ls -l" or ["ls", "-l"]
  716. timeout: seconds of command run, works on when stream is False
  717. stream: bool used for long running process.
  718. Returns:
  719. (output, exit_code) when stream is False
  720. requests.Response when stream is True, you have to close it after using
  721. Raises:
  722. RuntimeError
  723. For atx-agent is not support return exit code now.
  724. When command got something wrong, exit_code is always 1, otherwise exit_code is always 0
  725. """
  726. print("uiautomator2.__init__.shell:cmd=",cmdargs)
  727. if isinstance(cmdargs, (list, tuple)):
  728. cmdargs = list2cmdline(cmdargs)
  729. if stream:
  730. return self._reqsess.get(
  731. self.path2url("/shell/stream"),
  732. params={"command": cmdargs},
  733. stream=True)
  734. ret = self._reqsess.post(
  735. self.path2url('/shell'),
  736. data={
  737. 'command': cmdargs,
  738. 'timeout': str(timeout)
  739. }, timeout=timeout+10)
  740. if ret.status_code != 200:
  741. raise RuntimeError(
  742. "device agent responds with an error code %d" %
  743. ret.status_code, ret.text)
  744. resp = ret.json()
  745. exit_code = 1 if resp.get('error') else 0
  746. exit_code = resp.get('exitCode', exit_code)
  747. shell_response = namedtuple("ShellResponse", ("output", "exit_code"))
  748. return shell_response(resp.get('output'), exit_code)
  749. def adb_shell(self, *args):
  750. """
  751. Example:
  752. adb_shell('pwd')
  753. adb_shell('ls', '-l')
  754. adb_shell('ls -l')
  755. Returns:
  756. string for stdout merged with stderr, after the entire shell command is completed.
  757. """
  758. # print(
  759. # "DeprecatedWarning: adb_shell is deprecated, use: output, exit_code = shell(['ls', '-l']) instead"
  760. # )
  761. cmdline = args[0] if len(args) == 1 else list2cmdline(args)
  762. return self.shell(cmdline)[0]
  763. def app_start(self,
  764. pkg_name,
  765. activity=None,
  766. extras={},
  767. wait=True,
  768. stop=False,
  769. unlock=False):
  770. """ Launch application
  771. Args:
  772. pkg_name (str): package name
  773. activity (str): app activity
  774. stop (bool): Stop app before starting the activity. (require activity)
  775. """
  776. if unlock:
  777. self.unlock()
  778. if activity:
  779. # -D: enable debugging
  780. # -W: wait for launch to complete
  781. # -S: force stop the target app before starting the activity
  782. # --user <USER_ID> | current: Specify which user to run as; if not
  783. # specified then run as the current user.
  784. # -e <EXTRA_KEY> <EXTRA_STRING_VALUE>
  785. # --ei <EXTRA_KEY> <EXTRA_INT_VALUE>
  786. # --ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE>
  787. args = [
  788. 'am', 'start', '-a', 'android.intent.action.MAIN', '-c',
  789. 'android.intent.category.LAUNCHER'
  790. ]
  791. if wait:
  792. args.append('-W')
  793. if stop:
  794. args.append('-S')
  795. args += ['-n', '{}/{}'.format(pkg_name, activity)]
  796. # -e --ez
  797. extra_args = []
  798. for k, v in extras.items():
  799. if isinstance(v, bool):
  800. extra_args.extend(['--ez', k, 'true' if v else 'false'])
  801. elif isinstance(v, int):
  802. extra_args.extend(['--ei', k, str(v)])
  803. else:
  804. extra_args.extend(['-e', k, v])
  805. args += extra_args
  806. # 'am', 'start', '-W', '-n', '{}/{}'.format(pkg_name, activity))
  807. self.shell(args)
  808. else:
  809. if stop:
  810. self.app_stop(pkg_name)
  811. self.shell([
  812. 'monkey', '-p', pkg_name, '-c',
  813. 'android.intent.category.LAUNCHER', '1'
  814. ])
  815. @retry(EnvironmentError, delay=.5, tries=3, jitter=.1)
  816. def current_app(self):
  817. """
  818. Returns:
  819. dict(package, activity, pid?)
  820. Raises:
  821. EnvironementError
  822. For developer:
  823. Function reset_uiautomator need this function, so can't use jsonrpc here.
  824. """
  825. # Related issue: https://github.com/openatx/uiautomator2/issues/200
  826. # $ adb shell dumpsys window windows
  827. # Example output:
  828. # mCurrentFocus=Window{41b37570 u0 com.incall.apps.launcher/com.incall.apps.launcher.Launcher}
  829. # mFocusedApp=AppWindowToken{422df168 token=Token{422def98 ActivityRecord{422dee38 u0 com.example/.UI.play.PlayActivity t14}}}
  830. # Regexp
  831. # r'mFocusedApp=.*ActivityRecord{\w+ \w+ (?P<package>.*)/(?P<activity>.*) .*'
  832. # r'mCurrentFocus=Window{\w+ \w+ (?P<package>.*)/(?P<activity>.*)\}')
  833. _focusedRE = re.compile(
  834. r'mCurrentFocus=Window{.*\s+(?P<package>[^\s]+)/(?P<activity>[^\s]+)\}'
  835. )
  836. m = _focusedRE.search(self.shell(['dumpsys', 'window', 'windows'])[0])
  837. if m:
  838. return dict(
  839. package=m.group('package'), activity=m.group('activity'))
  840. # try: adb shell dumpsys activity top
  841. _activityRE = re.compile(
  842. r'ACTIVITY (?P<package>[^\s]+)/(?P<activity>[^/\s]+) \w+ pid=(?P<pid>\d+)'
  843. )
  844. output, _ = self.shell(['dumpsys', 'activity', 'top'])
  845. ms = _activityRE.finditer(output)
  846. ret = None
  847. for m in ms:
  848. ret = dict(
  849. package=m.group('package'),
  850. activity=m.group('activity'),
  851. pid=int(m.group('pid')))
  852. if ret: # get last result
  853. return ret
  854. raise EnvironmentError("Couldn't get focused app")
  855. def wait_activity(self, activity, timeout=10):
  856. """ wait activity
  857. Args:
  858. activity (str): name of activity
  859. timeout (float): max wait time
  860. Returns:
  861. bool of activity
  862. """
  863. deadline = time.time() + timeout
  864. while time.time() < deadline:
  865. current_activity = self.current_app().get('activity')
  866. if activity == current_activity:
  867. return True
  868. time.sleep(.5)
  869. return False
  870. def app_stop(self, pkg_name):
  871. """ Stop one application: am force-stop"""
  872. self.shell(['am', 'force-stop', pkg_name])
  873. def app_stop_all(self, excludes=[]):
  874. """ Stop all third party applications
  875. Args:
  876. excludes (list): apps that do now want to kill
  877. Returns:
  878. a list of killed apps
  879. """
  880. our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']
  881. output, _ = self.shell(['pm', 'list', 'packages', '-3'])
  882. pkgs = re.findall('package:([^\s]+)', output)
  883. process_names = re.findall('([^\s]+)$', self.shell('ps')[0], re.M)
  884. kill_pkgs = set(pkgs).intersection(process_names).difference(our_apps +
  885. excludes)
  886. kill_pkgs = list(kill_pkgs)
  887. for pkg_name in kill_pkgs:
  888. self.app_stop(pkg_name)
  889. return kill_pkgs
  890. def app_clear(self, pkg_name):
  891. """ Stop and clear app data: pm clear """
  892. self.shell(['pm', 'clear', pkg_name])
  893. def app_uninstall(self, pkg_name):
  894. """ Uninstall an app """
  895. self.shell(["pm", "uninstall", pkg_name])
  896. def app_uninstall_all(self, excludes=[], verbose=False):
  897. """ Uninstall all apps """
  898. our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']
  899. output, _ = self.shell(['pm', 'list', 'packages', '-3'])
  900. pkgs = re.findall('package:([^\s]+)', output)
  901. pkgs = set(pkgs).difference(our_apps + excludes)
  902. pkgs = list(pkgs)
  903. for pkg_name in pkgs:
  904. if verbose:
  905. print("uninstalling", pkg_name)
  906. self.app_uninstall(pkg_name)
  907. return pkgs
  908. def unlock(self):
  909. """ unlock screen """
  910. self.open_identify()
  911. self._default_session.press("home")
  912. def open_identify(self, theme='black'):
  913. """
  914. Args:
  915. theme (str): black or red
  916. """
  917. self.shell([
  918. 'am', 'start', '-W', '-n',
  919. 'com.github.uiautomator/.IdentifyActivity', '-e', 'theme', theme
  920. ])
  921. def _pidof_app(self, pkg_name):
  922. """
  923. Return pid of package name
  924. """
  925. text = self._reqsess.get(self.path2url('/pidof/' + pkg_name)).text
  926. if text.isdigit():
  927. return int(text)
  928. def push_url(self, url, dst, mode=0o644):
  929. """
  930. Args:
  931. url (str): http url address
  932. dst (str): destination
  933. mode (str): file mode
  934. Raises:
  935. FileNotFoundError(py3) OSError(py2)
  936. """
  937. modestr = oct(mode).replace('o', '')
  938. r = self._reqsess.post(
  939. self.path2url('/download'),
  940. data={
  941. 'url': url,
  942. 'filepath': dst,
  943. 'mode': modestr
  944. })
  945. if r.status_code != 200:
  946. raise IOError("push-url", "%s -> %s" % (url, dst), r.text)
  947. key = r.text.strip()
  948. while 1:
  949. r = self._reqsess.get(self.path2url('/download/' + key))
  950. jdata = r.json()
  951. message = jdata.get('message')
  952. if message == 'downloaded':
  953. log_print("downloaded")
  954. break
  955. elif message == 'downloading':
  956. progress = jdata.get('progress')
  957. if progress:
  958. copied_size = progress.get('copiedSize')
  959. total_size = progress.get('totalSize')
  960. log_print("{} {} / {}".format(
  961. message, humanize.naturalsize(copied_size),
  962. humanize.naturalsize(total_size)))
  963. else:
  964. log_print("downloading")
  965. else:
  966. log_print("unknown json:" + str(jdata))
  967. raise IOError(message)
  968. time.sleep(1)
  969. def push(self, src, dst, mode=0o644):
  970. """
  971. Args:
  972. src (path or fileobj): source file
  973. dst (str): destination can be folder or file path
  974. Returns:
  975. dict object, for example:
  976. {"mode": "0660", "size": 63, "target": "/sdcard/ABOUT.rst"}
  977. Since chmod may fail in android, the result "mode" may not same with input args(mode)
  978. Raises:
  979. IOError(if push got something wrong)
  980. """
  981. modestr = oct(mode).replace('o', '')
  982. pathname = self.path2url('/upload/' + dst.lstrip('/'))
  983. if isinstance(src, six.string_types):
  984. src = open(src, 'rb')
  985. r = self._reqsess.post(
  986. pathname, data={'mode': modestr}, files={'file': src})
  987. if r.status_code == 200:
  988. return r.json()
  989. raise IOError("push", "%s -> %s" % (src, dst), r.text)
  990. def pull(self, src, dst):
  991. """
  992. Pull file from device to local
  993. Raises:
  994. FileNotFoundError(py3) OSError(py2)
  995. Require atx-agent >= 0.0.9
  996. """
  997. pathname = self.path2url("/raw/" + src.lstrip("/"))
  998. r = self._reqsess.get(pathname, stream=True)
  999. if r.status_code != 200:
  1000. raise FileNotFoundError("pull", src, r.text)
  1001. with open(dst, 'wb') as f:
  1002. shutil.copyfileobj(r.raw, f)
  1003. def pull_content(self, src):
  1004. """
  1005. Read remote file content
  1006. Raises:
  1007. FileNotFoundError
  1008. """
  1009. pathname = self.path2url("/raw/" + src.lstrip("/"))
  1010. r = self._reqsess.get(pathname)
  1011. if r.status_code != 200:
  1012. raise FileNotFoundError("pull", src, r.text)
  1013. return r.content
  1014. @property
  1015. def screenshot_uri(self):
  1016. return 'http://%s:%d/screenshot/0' % (self._host, self._port)
  1017. def screenshot(self, *args, **kwargs):
  1018. """
  1019. Take screenshot of device
  1020. Returns:
  1021. PIL.Image
  1022. """
  1023. return self.session().screenshot(*args, **kwargs)
  1024. @property
  1025. def device_info(self):
  1026. if self.__devinfo:
  1027. return self.__devinfo
  1028. self.__devinfo = self._reqsess.get(self.path2url('/info')).json()
  1029. return self.__devinfo
  1030. def app_info(self, pkg_name):
  1031. """
  1032. Get app info
  1033. Args:
  1034. pkg_name (str): package name
  1035. Return example:
  1036. {
  1037. "mainActivity": "com.github.uiautomator.MainActivity",
  1038. "label": "ATX",
  1039. "versionName": "1.1.7",
  1040. "versionCode": 1001007,
  1041. "size":1760809
  1042. }
  1043. Raises:
  1044. UiaError
  1045. """
  1046. url = self.path2url('/packages/{0}/info'.format(pkg_name))
  1047. resp = self._reqsess.get(url)
  1048. resp.raise_for_status()
  1049. resp = resp.json()
  1050. if not resp.get('success'):
  1051. raise UiaError(resp.get('description', 'unknown'))
  1052. return resp.get('data')
  1053. def app_icon(self, pkg_name):
  1054. """
  1055. Returns:
  1056. PIL.Image
  1057. Raises:
  1058. UiaError
  1059. """
  1060. from PIL import Image
  1061. url = self.path2url('/packages/{0}/icon'.format(pkg_name))
  1062. resp = self._reqsess.get(url)
  1063. resp.raise_for_status()
  1064. return Image.open(io.BytesIO(resp.content))
  1065. @property
  1066. def wlan_ip(self):
  1067. return self._reqsess.get(self.path2url("/wlan/ip")).text.strip()
  1068. def disable_popups(self, enable=True):
  1069. """
  1070. Automatic click all popups
  1071. TODO: need fix
  1072. """
  1073. raise NotImplementedError()
  1074. # self.watcher
  1075. if enable:
  1076. self.jsonrpc.setAccessibilityPatterns({
  1077. "com.android.packageinstaller":
  1078. [u"确定", u"安装", u"下一步", u"好", u"允许", u"我知道"],
  1079. "com.miui.securitycenter": [u"继续安装"], # xiaomi
  1080. "com.lbe.security.miui": [u"允许"], # xiaomi
  1081. "android": [u"好", u"安装"], # vivo
  1082. "com.huawei.systemmanager": [u"立即删除"], # huawei
  1083. "com.android.systemui": [u"同意"], # 锤子
  1084. })
  1085. else:
  1086. self.jsonrpc.setAccessibilityPatterns({})
  1087. def session(self, pkg_name=None, attach=False, launch_timeout=None):
  1088. """
  1089. Create a new session
  1090. Args:
  1091. pkg_name (str): android package name
  1092. attach (bool): attach to already running app
  1093. launch_timeout (int): launch timeout
  1094. Raises:
  1095. requests.HTTPError, SessionBrokenError
  1096. """
  1097. if pkg_name is None:
  1098. return self._default_session
  1099. if not attach:
  1100. request_data = {"flags": "-W -S"}
  1101. if launch_timeout:
  1102. request_data["timeout"] = str(launch_timeout)
  1103. resp = self._reqsess.post(
  1104. self.path2url("/session/" + pkg_name), data=request_data)
  1105. if resp.status_code == 410: # Gone
  1106. raise SessionBrokenError(pkg_name, resp.text)
  1107. resp.raise_for_status()
  1108. jsondata = resp.json()
  1109. if not jsondata["success"]:
  1110. raise SessionBrokenError("app launch failed",
  1111. jsondata["error"], jsondata["output"])
  1112. time.sleep(2.5) # wait launch finished, maybe no need
  1113. pid = self._pidof_app(pkg_name)
  1114. if not pid:
  1115. raise SessionBrokenError(pkg_name)
  1116. return Session(self, pkg_name, pid)
  1117. def __getattr__(self, attr):
  1118. if attr in self._cached_plugins:
  1119. return self._cached_plugins[attr]
  1120. if attr.startswith('ext_'):
  1121. plugin_name = attr[4:]
  1122. if plugin_name not in self.__plugins:
  1123. if plugin_name == 'xpath':
  1124. import ssat_sdk.uiautomator2.ext.xpath as xpath
  1125. xpath.init()
  1126. else:
  1127. raise ValueError(
  1128. "plugin \"%s\" not registed" % plugin_name)
  1129. func, args, kwargs = self.__plugins[plugin_name]
  1130. obj = functools.partial(func, self)(*args, **kwargs)
  1131. self._cached_plugins[attr] = obj
  1132. return obj
  1133. try:
  1134. return getattr(self._default_session, attr)
  1135. except AttributeError:
  1136. raise AttributeError(
  1137. "'Session or UIAutomatorServer' object has no attribute '%s'" %
  1138. attr)
  1139. def __call__(self, **kwargs):
  1140. return self._default_session(**kwargs)
  1141. class AdbShell(object):
  1142. def __init__(self, shellfn):
  1143. """
  1144. Args:
  1145. shellfn: Shell function
  1146. """
  1147. self.shell = shellfn
  1148. def wmsize(self):
  1149. """ get window size
  1150. Returns:
  1151. (width, height)
  1152. """
  1153. output, _ = self.shell("wm size")
  1154. m = re.match(r"Physical size: (\d+)x(\d+)", output)
  1155. if m:
  1156. return map(int, m.groups())
  1157. raise RuntimeError("Can't parse wm size: " + output)
  1158. def is_screen_on(self):
  1159. output, _ = self.shell("dumpsys power")
  1160. return 'mHoldingDisplaySuspendBlocker=true' in output
  1161. def keyevent(self, v):
  1162. """
  1163. Args:
  1164. v: eg home wakeup back
  1165. """
  1166. v = v.upper()
  1167. self.shell("input keyevent " + v)
  1168. def _adjust_pos(self, x, y, w=None, h=None):
  1169. if x < 1:
  1170. x = x * w
  1171. if y < 1:
  1172. y = y * h
  1173. return (x, y)
  1174. def swipe(self, x0, y0, x1, y1):
  1175. w, h = None, None
  1176. if x0 < 1 or y0 < 1 or x1 < 1 or y1 < 1:
  1177. w, h = self.wmsize()
  1178. x0, y0 = self._adjust_pos(x0, y0, w, h)
  1179. x1, y1 = self._adjust_pos(x1, y1, w, h)
  1180. self.shell("input swipe %d %d %d %d" % (x0, y0, x1, y1))