session.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. # coding: utf-8
  2. #
  3. from __future__ import absolute_import, print_function
  4. import base64
  5. import io
  6. import logging
  7. import re
  8. import time
  9. import warnings
  10. import xml.dom.minidom
  11. import requests
  12. import six
  13. from retry import retry
  14. from ssat_sdk.uiautomator2.exceptions import (NullPointerExceptionError,
  15. UiObjectNotFoundError)
  16. from ssat_sdk.uiautomator2.utils import Exists, U, check_alive, hooks_wrap, intersect
  17. _INPUT_METHOD_RE = re.compile(r'mCurMethodId=([-_./\w]+)')
  18. _fail_prompt_enabled = False
  19. def set_fail_prompt(enable=True):
  20. """
  21. When Element click through Exception, Prompt user to decide
  22. """
  23. global _fail_prompt_enabled
  24. _fail_prompt_enabled = enable
  25. def _failprompt(fn):
  26. def _inner(self, *args, **kwargs):
  27. if not _fail_prompt_enabled:
  28. return fn(self, *args, **kwargs)
  29. from ssat_sdk.uiautomator2 import messagebox
  30. try:
  31. return fn(self, *args, **kwargs)
  32. except UiObjectNotFoundError as e:
  33. result = messagebox.retryskipabort(str(e), 30)
  34. if result == 'retry':
  35. return _inner(self, *args, **kwargs)
  36. elif result == 'skip':
  37. return True
  38. else:
  39. raise
  40. return _inner
  41. class Session(object):
  42. __orientation = ( # device orientation
  43. (0, "natural", "n", 0), (1, "left", "l", 90),
  44. (2, "upsidedown", "u", 180), (3, "right", "r", 270))
  45. def __init__(self, server, pkg_name=None, pid=None):
  46. self.server = server
  47. self._pkg_name = pkg_name
  48. self._pid = pid
  49. self._jsonrpc = server.jsonrpc
  50. if pid and pkg_name:
  51. jsonrpc_url = server.path2url('/session/%d:%s/jsonrpc/0' %
  52. (pid, pkg_name))
  53. self._jsonrpc = server.setup_jsonrpc(jsonrpc_url)
  54. # hot fix for session missing shell function
  55. self.shell = self.server.shell
  56. def __repr__(self):
  57. if self._pid and self._pkg_name:
  58. return "<uiautomator2.Session pid:%d pkgname:%s>" % (
  59. self._pid, self._pkg_name)
  60. return super(Session, self).__repr__()
  61. def __enter__(self):
  62. return self
  63. def __exit__(self, exc_type, exc_val, exc_tb):
  64. self.close()
  65. def implicitly_wait(self, seconds=None):
  66. """set default wait timeout
  67. Args:
  68. seconds(float): to wait element show up
  69. """
  70. if seconds is not None:
  71. self.server.wait_timeout = seconds
  72. return self.server.wait_timeout
  73. def close(self):
  74. """ close app """
  75. if self._pkg_name:
  76. self.server.app_stop(self._pkg_name)
  77. def running(self):
  78. """
  79. Check is session is running. return bool
  80. """
  81. if self._pid and self._pkg_name:
  82. ping_url = self.server.path2url('/session/%d:%s/ping' %
  83. (self._pid, self._pkg_name))
  84. return self.server._reqsess.get(ping_url).text.strip() == 'pong'
  85. # warnings.warn("pid and pkg_name is not set, ping will always return True", Warning, stacklevel=1)
  86. return True
  87. @property
  88. def jsonrpc(self):
  89. return self._jsonrpc
  90. @property
  91. def pos_rel2abs(self):
  92. size = []
  93. def convert(x, y):
  94. assert x >= 0
  95. assert y >= 0
  96. if (x < 1 or y < 1) and not size:
  97. size.extend(
  98. self.server.window_size()) # size will be [width, height]
  99. if x < 1:
  100. x = int(size[0] * x)
  101. if y < 1:
  102. y = int(size[1] * y)
  103. return x, y
  104. return convert
  105. def make_toast(self, text, duration=1.0):
  106. """ Show toast
  107. Args:
  108. text (str): text to show
  109. duration (float): seconds of display
  110. """
  111. warnings.warn(
  112. "Use d.toast.show(text, duration) instead.",
  113. DeprecationWarning,
  114. stacklevel=2)
  115. return self.jsonrpc.makeToast(text, duration * 1000)
  116. @property
  117. def toast(self):
  118. obj = self
  119. class Toast(object):
  120. def get_message(self,
  121. wait_timeout=10,
  122. cache_timeout=10,
  123. default=None):
  124. """
  125. Args:
  126. wait_timeout: seconds of max wait time if toast now show right now
  127. cache_timeout: return immediately if toast showed in recent $cache_timeout
  128. default: default messsage to return when no toast show up
  129. Returns:
  130. None or toast message
  131. """
  132. deadline = time.time() + wait_timeout
  133. while 1:
  134. message = obj.jsonrpc.getLastToast(cache_timeout * 1000)
  135. if message:
  136. return message
  137. if time.time() > deadline:
  138. return default
  139. time.sleep(.5)
  140. def reset(self):
  141. return obj.jsonrpc.clearLastToast()
  142. def show(self, text, duration=1.0):
  143. return obj.jsonrpc.makeToast(text, duration * 1000)
  144. return Toast()
  145. @check_alive
  146. def set_fastinput_ime(self, enable=True):
  147. """ Enable of Disable FastInputIME """
  148. fast_ime = 'com.github.uiautomator/.FastInputIME'
  149. if enable:
  150. self.server.shell(['ime', 'enable', fast_ime])
  151. self.server.shell(['ime', 'set', fast_ime])
  152. else:
  153. self.server.shell(['ime', 'disable', fast_ime])
  154. @check_alive
  155. def send_keys(self, text):
  156. """
  157. Raises:
  158. EnvironmentError
  159. """
  160. try:
  161. self.wait_fastinput_ime()
  162. btext = U(text).encode('utf-8')
  163. base64text = base64.b64encode(btext).decode()
  164. self.server.shell([
  165. 'am', 'broadcast', '-a', 'ADB_INPUT_TEXT', '--es', 'text',
  166. base64text
  167. ])
  168. return True
  169. except EnvironmentError:
  170. warnings.warn(
  171. "set FastInputIME failed. use \"d(focused=True).set_text instead\"",
  172. Warning)
  173. return self(focused=True).set_text(text)
  174. # warnings.warn("set FastInputIME failed. use \"adb shell input text\" instead", Warning)
  175. # self.server.adb_shell("input", "text", text.replace(" ", "%s"))
  176. @check_alive
  177. def send_action(self, code):
  178. """
  179. Simulate input method edito code
  180. Args:
  181. code (str or int): input method editor code
  182. Examples:
  183. send_action("search"), send_action(3)
  184. Refs:
  185. https://developer.android.com/reference/android/view/inputmethod/EditorInfo
  186. """
  187. self.wait_fastinput_ime()
  188. __alias = {
  189. "go": 2,
  190. "search": 3,
  191. "send": 4,
  192. "next": 5,
  193. "done": 6,
  194. "previous": 7,
  195. }
  196. if isinstance(code, six.string_types):
  197. code = __alias.get(code, code)
  198. self.server.shell(['am', 'broadcast', '-a', 'ADB_EDITOR_CODE', '--ei', 'code', str(code)])
  199. @check_alive
  200. def clear_text(self):
  201. """ clear text
  202. Raises:
  203. EnvironmentError
  204. """
  205. try:
  206. self.wait_fastinput_ime()
  207. self.server.shell(['am', 'broadcast', '-a', 'ADB_CLEAR_TEXT'])
  208. except EnvironmentError:
  209. # for Android simulator
  210. self(focused=True).clear_text()
  211. def wait_fastinput_ime(self, timeout=5.0):
  212. """ wait FastInputIME is ready
  213. Args:
  214. timeout(float): maxium wait time
  215. Raises:
  216. EnvironmentError
  217. """
  218. if not self.server.serial: # maybe simulator eg: genymotion, 海马玩模拟器
  219. raise EnvironmentError("Android simulator is not supported.")
  220. deadline = time.time() + timeout
  221. while time.time() < deadline:
  222. ime_id, shown = self.current_ime()
  223. if ime_id != "com.github.uiautomator/.FastInputIME":
  224. self.set_fastinput_ime(True)
  225. time.sleep(0.5)
  226. continue
  227. if shown:
  228. return True
  229. time.sleep(0.2)
  230. raise EnvironmentError("FastInputIME started failed")
  231. def current_ime(self):
  232. """ Current input method
  233. Returns:
  234. (method_id(str), shown(bool)
  235. Example output:
  236. ("com.github.uiautomator/.FastInputIME", True)
  237. """
  238. dim, _ = self.server.shell(['dumpsys', 'input_method'])
  239. m = _INPUT_METHOD_RE.search(dim)
  240. method_id = None if not m else m.group(1)
  241. shown = "mInputShown=true" in dim
  242. return (method_id, shown)
  243. def tap(self, x, y):
  244. """
  245. alias of click
  246. """
  247. self.click(x, y)
  248. @property
  249. def touch(self):
  250. """
  251. ACTION_DOWN: 0 ACTION_MOVE: 2
  252. touch.down(x, y)
  253. touch.move(x, y)
  254. touch.up()
  255. """
  256. ACTION_DOWN = 0
  257. ACTION_MOVE = 2
  258. ACTION_UP = 1
  259. obj = self
  260. class _Touch(object):
  261. def down(self, x, y):
  262. obj.jsonrpc.injectInputEvent(ACTION_DOWN, x, y, 0)
  263. def move(self, x, y):
  264. obj.jsonrpc.injectInputEvent(ACTION_MOVE, x, y, 0)
  265. def up(self, x=0, y=0):
  266. """ ACTION_UP x, y seems no use """
  267. obj.jsonrpc.injectInputEvent(ACTION_UP, x, y, 0)
  268. return _Touch()
  269. def click(self, x, y):
  270. """
  271. click position
  272. """
  273. x, y = self.pos_rel2abs(x, y)
  274. self._click(x, y)
  275. @hooks_wrap
  276. def _click(self, x, y):
  277. self.jsonrpc.click(x, y)
  278. if self.server.click_post_delay: # click code delay
  279. time.sleep(self.server.click_post_delay)
  280. def double_click(self, x, y, duration=0.1):
  281. """
  282. double click position
  283. """
  284. x, y = self.pos_rel2abs(x, y)
  285. self.touch.down(x, y)
  286. self.touch.up(x, y)
  287. time.sleep(duration)
  288. self.click(x, y) # use click last is for htmlreport
  289. def long_click(self, x, y, duration=None):
  290. '''long click at arbitrary coordinates.
  291. Args:
  292. duration (float): seconds of pressed
  293. '''
  294. if not duration:
  295. duration = 0.5
  296. x, y = self.pos_rel2abs(x, y)
  297. return self._long_click(x, y, duration)
  298. @hooks_wrap
  299. def _long_click(self, x, y, duration):
  300. self.touch.down(x, y)
  301. # self.touch.move(x, y) # maybe can fix
  302. time.sleep(duration)
  303. self.touch.up(x, y)
  304. return self
  305. def swipe(self, fx, fy, tx, ty, duration=0.1, steps=None):
  306. """
  307. Args:
  308. fx, fy: from position
  309. tx, ty: to position
  310. duration (float): duration
  311. steps: 1 steps is about 5ms, if set, duration will be ignore
  312. Documents:
  313. uiautomator use steps instead of duration
  314. As the document say: Each step execution is throttled to 5ms per step.
  315. Links:
  316. https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#swipe%28int,%20int,%20int,%20int,%20int%29
  317. """
  318. rel2abs = self.pos_rel2abs
  319. fx, fy = rel2abs(fx, fy)
  320. tx, ty = rel2abs(tx, ty)
  321. if not steps:
  322. steps = int(duration * 200)
  323. self._swipe(fx, fy, tx, ty, steps)
  324. @hooks_wrap
  325. def _swipe(self, fx, fy, tx, ty, steps):
  326. return self.jsonrpc.swipe(fx, fy, tx, ty, steps)
  327. def swipe_points(self, points, duration=0.5):
  328. """
  329. Args:
  330. points: is point array containg at least one point object. eg [[200, 300], [210, 320]]
  331. duration: duration to inject between two points
  332. Links:
  333. https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#swipe(android.graphics.Point[], int)
  334. """
  335. ppoints = []
  336. rel2abs = self.pos_rel2abs
  337. for p in points:
  338. x, y = rel2abs(p[0], p[1])
  339. ppoints.append(x)
  340. ppoints.append(y)
  341. return self.jsonrpc.swipePoints(ppoints, int(duration * 200))
  342. def drag(self, sx, sy, ex, ey, duration=0.5):
  343. '''Swipe from one point to another point.'''
  344. rel2abs = self.pos_rel2abs
  345. sx, sy = rel2abs(sx, sy)
  346. ex, ey = rel2abs(ex, ey)
  347. return self.jsonrpc.drag(sx, sy, ex, ey, int(duration * 200))
  348. @retry(
  349. (IOError, SyntaxError), delay=.5, tries=5, jitter=0.1,
  350. max_delay=1) # delay .5, .6, .7, .8 ...
  351. def screenshot(self, filename=None, format='pillow'):
  352. """
  353. Image format is JPEG
  354. Args:
  355. filename (str): saved filename
  356. format (string): used when filename is empty. one of "pillow" or "opencv"
  357. Raises:
  358. IOError, SyntaxError
  359. Examples:
  360. screenshot("saved.jpg")
  361. screenshot().save("saved.png")
  362. cv2.imwrite('saved.jpg', screenshot(format='opencv'))
  363. """
  364. r = requests.get(self.server.screenshot_uri, timeout=10)
  365. if filename:
  366. with open(filename, 'wb') as f:
  367. f.write(r.content)
  368. return filename
  369. elif format == 'pillow':
  370. from PIL import Image
  371. buff = io.BytesIO(r.content)
  372. return Image.open(buff)
  373. elif format == 'opencv':
  374. import cv2
  375. import numpy as np
  376. nparr = np.fromstring(r.content, np.uint8)
  377. return cv2.imdecode(nparr, cv2.IMREAD_COLOR)
  378. elif format == 'raw':
  379. return r.content
  380. else:
  381. raise RuntimeError("Invalid format " + format)
  382. def dump_hierarchy(self, compressed=False, pretty=False):
  383. """
  384. Args:
  385. shell (bool): use "adb shell uiautomator dump" to get hierarchy
  386. pretty (bool): format xml
  387. Same as
  388. content = self.jsonrpc.dumpWindowHierarchy(compressed, None)
  389. But through GET /dump/hierarchy will be more robust
  390. when dumpHierarchy fails, the atx-agent will restart uiautomator again, then retry
  391. """
  392. res = self.server._reqsess.get(self.server.path2url("/dump/hierarchy"))
  393. try:
  394. res.raise_for_status()
  395. except requests.HTTPError:
  396. logging.warning("request error: %s", res.text)
  397. raise
  398. content = res.json().get("result")
  399. if pretty and "\n " not in content:
  400. xml_text = xml.dom.minidom.parseString(content.encode("utf-8"))
  401. content = U(xml_text.toprettyxml(indent=' '))
  402. return content
  403. def freeze_rotation(self, freeze=True):
  404. '''freeze or unfreeze the device rotation in current status.'''
  405. self.jsonrpc.freezeRotation(freeze)
  406. def press(self, key, meta=None, duration=1.0):
  407. """
  408. press key via name or key code. Supported key name includes:
  409. home, back, left, right, up, down, center, menu, search, enter,
  410. delete(or del), recent(recent apps), volume_up, volume_down,
  411. volume_mute, camera, power.
  412. """
  413. if isinstance(key, int):
  414. time.sleep(duration)
  415. return self.jsonrpc.pressKeyCode(
  416. key, meta) if meta else self.server.jsonrpc.pressKeyCode(key)
  417. else:
  418. time.sleep(duration)
  419. return self.jsonrpc.pressKey(key)
  420. def pressKeyList(self, keyList):
  421. for key in keyList:
  422. self.press(key)
  423. def pressKeyTimes(self, key, times=1, duration=1.0):
  424. for i in range(times):
  425. self.press(key, duration=duration)
  426. def screen_on(self):
  427. self.jsonrpc.wakeUp()
  428. def screen_off(self):
  429. self.jsonrpc.sleep()
  430. @property
  431. def orientation(self):
  432. '''
  433. orienting the devie to left/right or natural.
  434. left/l: rotation=90 , displayRotation=1
  435. right/r: rotation=270, displayRotation=3
  436. natural/n: rotation=0 , displayRotation=0
  437. upsidedown/u: rotation=180, displayRotation=2
  438. '''
  439. return self.__orientation[self.info["displayRotation"]][1]
  440. def set_orientation(self, value):
  441. '''setter of orientation property.'''
  442. for values in self.__orientation:
  443. if value in values:
  444. # can not set upside-down until api level 18.
  445. self.jsonrpc.setOrientation(values[1])
  446. break
  447. else:
  448. raise ValueError("Invalid orientation.")
  449. @property
  450. def last_traversed_text(self):
  451. '''get last traversed text. used in webview for highlighted text.'''
  452. return self.jsonrpc.getLastTraversedText()
  453. def clear_traversed_text(self):
  454. '''clear the last traversed text.'''
  455. self.jsonrpc.clearLastTraversedText()
  456. def open_notification(self):
  457. return self.jsonrpc.openNotification()
  458. def open_quick_settings(self):
  459. return self.jsonrpc.openQuickSettings()
  460. def exists(self, **kwargs):
  461. return self(**kwargs).exists
  462. @property
  463. def xpath(self):
  464. return self.server.ext_xpath
  465. def watcher(self, name):
  466. obj = self
  467. class Watcher(object):
  468. def __init__(self):
  469. self.__selectors = []
  470. @property
  471. def triggered(self):
  472. return obj.server.jsonrpc.hasWatcherTriggered(name)
  473. def remove(self):
  474. obj.server.jsonrpc.removeWatcher(name)
  475. def when(self, **kwargs):
  476. self.__selectors.append(Selector(**kwargs))
  477. return self
  478. def click(self, **kwargs):
  479. target = Selector(**kwargs) if kwargs else self.__selectors[-1]
  480. obj.server.jsonrpc.registerClickUiObjectWatcher(
  481. name, self.__selectors, target)
  482. def press(self, *keys):
  483. """
  484. key (str): on of
  485. ("home", "back", "left", "right", "up", "down", "center",
  486. "search", "enter", "delete", "del", "recent", "volume_up",
  487. "menu", "volume_down", "volume_mute", "camera", "power")
  488. """
  489. obj.server.jsonrpc.registerPressKeyskWatcher(
  490. name, self.__selectors, keys)
  491. return Watcher()
  492. @property
  493. def watchers(self):
  494. obj = self
  495. class Watchers(list):
  496. def __init__(self):
  497. for watcher in obj.server.jsonrpc.getWatchers():
  498. self.append(watcher)
  499. @property
  500. def triggered(self):
  501. return obj.server.jsonrpc.hasAnyWatcherTriggered()
  502. def remove(self, name=None):
  503. if name:
  504. obj.server.jsonrpc.removeWatcher(name)
  505. else:
  506. for name in self:
  507. obj.server.jsonrpc.removeWatcher(name)
  508. def reset(self):
  509. obj.server.jsonrpc.resetWatcherTriggers()
  510. return self
  511. def run(self):
  512. obj.server.jsonrpc.runWatchers()
  513. return self
  514. @property
  515. def watched(self):
  516. return obj.server.jsonrpc.hasWatchedOnWindowsChange()
  517. @watched.setter
  518. def watched(self, b):
  519. """
  520. Args:
  521. b: boolean
  522. """
  523. assert isinstance(b, bool)
  524. obj.server.jsonrpc.runWatchersOnWindowsChange(b)
  525. return Watchers()
  526. @property
  527. def info(self):
  528. return self.jsonrpc.deviceInfo()
  529. def __call__(self, **kwargs):
  530. return UiObject(self, Selector(**kwargs))
  531. class Selector(dict):
  532. """The class is to build parameters for UiSelector passed to Android device.
  533. """
  534. __fields = {
  535. "text": (0x01, None), # MASK_TEXT,
  536. "textContains": (0x02, None), # MASK_TEXTCONTAINS,
  537. "textMatches": (0x04, None), # MASK_TEXTMATCHES,
  538. "textStartsWith": (0x08, None), # MASK_TEXTSTARTSWITH,
  539. "className": (0x10, None), # MASK_CLASSNAME
  540. "classNameMatches": (0x20, None), # MASK_CLASSNAMEMATCHES
  541. "description": (0x40, None), # MASK_DESCRIPTION
  542. "descriptionContains": (0x80, None), # MASK_DESCRIPTIONCONTAINS
  543. "descriptionMatches": (0x0100, None), # MASK_DESCRIPTIONMATCHES
  544. "descriptionStartsWith": (0x0200, None), # MASK_DESCRIPTIONSTARTSWITH
  545. "checkable": (0x0400, False), # MASK_CHECKABLE
  546. "checked": (0x0800, False), # MASK_CHECKED
  547. "clickable": (0x1000, False), # MASK_CLICKABLE
  548. "longClickable": (0x2000, False), # MASK_LONGCLICKABLE,
  549. "scrollable": (0x4000, False), # MASK_SCROLLABLE,
  550. "enabled": (0x8000, False), # MASK_ENABLED,
  551. "focusable": (0x010000, False), # MASK_FOCUSABLE,
  552. "focused": (0x020000, False), # MASK_FOCUSED,
  553. "selected": (0x040000, False), # MASK_SELECTED,
  554. "packageName": (0x080000, None), # MASK_PACKAGENAME,
  555. "packageNameMatches": (0x100000, None), # MASK_PACKAGENAMEMATCHES,
  556. "resourceId": (0x200000, None), # MASK_RESOURCEID,
  557. "resourceIdMatches": (0x400000, None), # MASK_RESOURCEIDMATCHES,
  558. "index": (0x800000, 0), # MASK_INDEX,
  559. "instance": (0x01000000, 0) # MASK_INSTANCE,
  560. }
  561. __mask, __childOrSibling, __childOrSiblingSelector = "mask", "childOrSibling", "childOrSiblingSelector"
  562. def __init__(self, **kwargs):
  563. super(Selector, self).__setitem__(self.__mask, 0)
  564. super(Selector, self).__setitem__(self.__childOrSibling, [])
  565. super(Selector, self).__setitem__(self.__childOrSiblingSelector, [])
  566. for k in kwargs:
  567. self[k] = kwargs[k]
  568. def __str__(self):
  569. """ remove useless part for easily debugger """
  570. selector = self.copy()
  571. selector.pop('mask')
  572. for key in ('childOrSibling', 'childOrSiblingSelector'):
  573. if not selector.get(key):
  574. selector.pop(key)
  575. args = []
  576. for (k, v) in selector.items():
  577. args.append(k + '=' + repr(v))
  578. return 'Selector [' + ', '.join(args) + ']'
  579. def __setitem__(self, k, v):
  580. if k in self.__fields:
  581. super(Selector, self).__setitem__(U(k), U(v))
  582. super(Selector, self).__setitem__(
  583. self.__mask, self[self.__mask] | self.__fields[k][0])
  584. else:
  585. raise ReferenceError("%s is not allowed." % k)
  586. def __delitem__(self, k):
  587. if k in self.__fields:
  588. super(Selector, self).__delitem__(k)
  589. super(Selector, self).__setitem__(
  590. self.__mask, self[self.__mask] & ~self.__fields[k][0])
  591. def clone(self):
  592. kwargs = dict((k, self[k]) for k in self if k not in [
  593. self.__mask, self.__childOrSibling, self.__childOrSiblingSelector
  594. ])
  595. selector = Selector(**kwargs)
  596. for v in self[self.__childOrSibling]:
  597. selector[self.__childOrSibling].append(v)
  598. for s in self[self.__childOrSiblingSelector]:
  599. selector[self.__childOrSiblingSelector].append(s.clone())
  600. return selector
  601. def child(self, **kwargs):
  602. self[self.__childOrSibling].append("child")
  603. self[self.__childOrSiblingSelector].append(Selector(**kwargs))
  604. return self
  605. def sibling(self, **kwargs):
  606. self[self.__childOrSibling].append("sibling")
  607. self[self.__childOrSiblingSelector].append(Selector(**kwargs))
  608. return self
  609. def update_instance(self, i):
  610. # update inside child instance
  611. if self[self.__childOrSiblingSelector]:
  612. self[self.__childOrSiblingSelector][-1]['instance'] = i
  613. else:
  614. self['instance'] = i
  615. class UiObject(object):
  616. def __init__(self, session, selector):
  617. self.session = session
  618. self.selector = selector
  619. self.jsonrpc = session.jsonrpc
  620. @property
  621. def wait_timeout(self):
  622. return self.session.server.wait_timeout
  623. @property
  624. def exists(self):
  625. '''check if the object exists in current window.'''
  626. return Exists(self)
  627. @property
  628. @retry(
  629. UiObjectNotFoundError, delay=.5, tries=3, jitter=0.1, logger=logging)
  630. def info(self):
  631. '''ui object info.'''
  632. try:
  633. return self.jsonrpc.objInfo(self.selector)
  634. except Exception,e:
  635. print("error:info is none")
  636. return None
  637. @_failprompt
  638. def click(self, timeout=None, offset=None):
  639. """
  640. Click UI element.
  641. Args:
  642. timeout: seconds wait element show up
  643. offset: (xoff, yoff) default (0.5, 0.5) -> center
  644. The click method does the same logic as java uiautomator does.
  645. 1. waitForExists 2. get VisibleBounds center 3. send click event
  646. Raises:
  647. UiObjectNotFoundError
  648. """
  649. self.must_wait(timeout=timeout)
  650. x, y = self.center(offset=offset)
  651. # ext.htmlreport need to comment bellow code
  652. # if info['clickable']:
  653. # return self.jsonrpc.click(self.selector)
  654. self.session.click(x, y)
  655. delay = self.session.server.click_post_delay
  656. if delay:
  657. time.sleep(delay)
  658. def bounds(self):
  659. """
  660. Returns:
  661. left_top_x, left_top_y, right_bottom_x, right_bottom_y
  662. """
  663. info = self.info
  664. bounds = info.get('visibleBounds') or info.get("bounds")
  665. lx, ly, rx, ry = bounds['left'], bounds['top'], bounds['right'], bounds['bottom']
  666. return (lx, ly, rx, ry)
  667. def center(self, offset=(0.5, 0.5)):
  668. """
  669. Args:
  670. offset: optional, (x_off, y_off)
  671. (0, 0) means left-top, (0.5, 0.5) means middle(Default)
  672. Return:
  673. center point (x, y)
  674. """
  675. lx, ly, rx, ry = self.bounds()
  676. if offset is None:
  677. offset = (0.5, 0.5) # default center
  678. xoff, yoff = offset
  679. width, height = rx - lx, ry - ly
  680. x = lx + width * xoff
  681. y = ly + height * yoff
  682. return (x, y)
  683. def click_gone(self, maxretry=10, interval=1.0):
  684. """
  685. Click until element is gone
  686. Args:
  687. maxretry (int): max click times
  688. interval (float): sleep time between clicks
  689. Return:
  690. Bool if element is gone
  691. """
  692. self.click_exists()
  693. while maxretry > 0:
  694. time.sleep(interval)
  695. if not self.exists:
  696. return True
  697. self.click_exists()
  698. maxretry -= 1
  699. return False
  700. def click_exists(self, timeout=0):
  701. try:
  702. self.click(timeout=timeout)
  703. return True
  704. except UiObjectNotFoundError:
  705. return False
  706. def long_click(self, duration=None, timeout=None):
  707. """
  708. Args:
  709. duration (float): seconds of pressed
  710. timeout (float): seconds wait element show up
  711. """
  712. # if info['longClickable'] and not duration:
  713. # return self.jsonrpc.longClick(self.selector)
  714. self.must_wait(timeout=timeout)
  715. x, y = self.center()
  716. return self.session.long_click(x, y, duration)
  717. def drag_to(self, *args, **kwargs):
  718. duration = kwargs.pop('duration', 0.5)
  719. timeout = kwargs.pop('timeout', None)
  720. self.must_wait(timeout=timeout)
  721. steps = int(duration * 200)
  722. if len(args) >= 2 or "x" in kwargs or "y" in kwargs:
  723. def drag2xy(x, y):
  724. x, y = self.session.pos_rel2abs(x,
  725. y) # convert percent position
  726. return self.jsonrpc.dragTo(self.selector, x, y, steps)
  727. return drag2xy(*args, **kwargs)
  728. return self.jsonrpc.dragTo(self.selector, Selector(**kwargs), steps)
  729. def swipe(self, direction, steps=10):
  730. """
  731. Performs the swipe action on the UiObject.
  732. Swipe from center
  733. Args:
  734. direction (str): one of ("left", "right", "up", "down")
  735. steps (int): move steps, one step is about 5ms
  736. percent: float between [0, 1]
  737. Note: percent require API >= 18
  738. # assert 0 <= percent <= 1
  739. """
  740. assert direction in ("left", "right", "up", "down")
  741. self.must_wait()
  742. info = self.info
  743. bounds = info.get('visibleBounds') or info.get("bounds")
  744. lx, ly, rx, ry = bounds['left'], bounds['top'], bounds['right'], bounds['bottom']
  745. cx, cy = (lx + rx) // 2, (ly + ry) // 2
  746. if direction == 'up':
  747. self.session.swipe(cx, cy, cx, ly, steps=steps)
  748. elif direction == 'down':
  749. self.session.swipe(cx, cy, cx, ry - 1, steps=steps)
  750. elif direction == 'left':
  751. self.session.swipe(cx, cy, lx, cy, steps=steps)
  752. elif direction == 'right':
  753. self.session.swipe(cx, cy, rx - 1, cy, steps=steps)
  754. # return self.jsonrpc.swipe(self.selector, direction, percent, steps)
  755. def gesture(self, start1, start2, end1, end2, steps=100):
  756. '''
  757. perform two point gesture.
  758. Usage:
  759. d().gesture(startPoint1, startPoint2, endPoint1, endPoint2, steps)
  760. '''
  761. rel2abs = self.session.pos_rel2abs
  762. def point(x=0, y=0):
  763. x, y = rel2abs(x, y)
  764. return {"x": x, "y": y}
  765. def ctp(pt):
  766. return point(*pt) if type(pt) == tuple else pt
  767. s1, s2, e1, e2 = ctp(start1), ctp(start2), ctp(end1), ctp(end2)
  768. return self.jsonrpc.gesture(self.selector, s1, s2, e1, e2, steps)
  769. def pinch_in(self, percent=100, steps=50):
  770. return self.jsonrpc.pinchIn(self.selector, percent, steps)
  771. def pinch_out(self, percent=100, steps=50):
  772. return self.jsonrpc.pinchOut(self.selector, percent, steps)
  773. def wait(self, exists=True, timeout=None):
  774. """
  775. Wait until UI Element exists or gone
  776. Args:
  777. timeout (float): wait element timeout
  778. Example:
  779. d(text="Clock").wait()
  780. d(text="Settings").wait("gone") # wait until it's gone
  781. """
  782. if timeout is None:
  783. timeout = self.wait_timeout
  784. http_wait = timeout + 10
  785. if exists:
  786. try:
  787. return self.jsonrpc.waitForExists(
  788. self.selector, int(timeout * 1000), http_timeout=http_wait)
  789. except requests.ReadTimeout as e:
  790. warnings.warn("waitForExists readTimeout: %s" %
  791. e, RuntimeWarning)
  792. return self.exists()
  793. else:
  794. try:
  795. return self.jsonrpc.waitUntilGone(
  796. self.selector, int(timeout * 1000), http_timeout=http_wait)
  797. except requests.ReadTimeout as e:
  798. warnings.warn("waitForExists readTimeout: %s" %
  799. e, RuntimeWarning)
  800. return not self.exists()
  801. def wait_gone(self, timeout=None):
  802. """ wait until ui gone
  803. Args:
  804. timeout (float): wait element gone timeout
  805. Returns:
  806. bool if element gone
  807. """
  808. timeout = timeout or self.wait_timeout
  809. return self.wait(exists=False, timeout=timeout)
  810. def must_wait(self, exists=True, timeout=None):
  811. """ wait and if not found raise UiObjectNotFoundError """
  812. if not self.wait(exists, timeout):
  813. raise UiObjectNotFoundError({'code': -32002, 'method': 'wait'})
  814. def send_keys(self, text):
  815. """ alias of set_text """
  816. return self.set_text(text)
  817. def set_text(self, text, timeout=None):
  818. self.must_wait(timeout=timeout)
  819. if not text:
  820. return self.jsonrpc.clearTextField(self.selector)
  821. else:
  822. return self.jsonrpc.setText(self.selector, text)
  823. def get_text(self, timeout=None):
  824. """ get text from field """
  825. self.must_wait(timeout=timeout)
  826. return self.jsonrpc.getText(self.selector)
  827. def clear_text(self, timeout=None):
  828. self.must_wait(timeout=timeout)
  829. return self.set_text(None)
  830. def child(self, **kwargs):
  831. return UiObject(self.session, self.selector.clone().child(**kwargs))
  832. def sibling(self, **kwargs):
  833. return UiObject(self.session, self.selector.clone().sibling(**kwargs))
  834. child_selector, from_parent = child, sibling
  835. def child_by_text(self, txt, **kwargs):
  836. if "allow_scroll_search" in kwargs:
  837. allow_scroll_search = kwargs.pop("allow_scroll_search")
  838. name = self.jsonrpc.childByText(self.selector, Selector(**kwargs),
  839. txt, allow_scroll_search)
  840. else:
  841. name = self.jsonrpc.childByText(self.selector, Selector(**kwargs),
  842. txt)
  843. return UiObject(self.session, name)
  844. def child_by_description(self, txt, **kwargs):
  845. # need test
  846. if "allow_scroll_search" in kwargs:
  847. allow_scroll_search = kwargs.pop("allow_scroll_search")
  848. name = self.jsonrpc.childByDescription(self.selector,
  849. Selector(**kwargs), txt,
  850. allow_scroll_search)
  851. else:
  852. name = self.jsonrpc.childByDescription(self.selector,
  853. Selector(**kwargs), txt)
  854. return UiObject(self.session, name)
  855. def child_by_instance(self, inst, **kwargs):
  856. # need test
  857. return UiObject(self.session,
  858. self.jsonrpc.childByInstance(self.selector,
  859. Selector(**kwargs), inst))
  860. def parent(self):
  861. # android-uiautomator-server not implemented
  862. # In UIAutomator, UIObject2 has getParent() method
  863. # https://developer.android.com/reference/android/support/test/uiautomator/UiObject2.html
  864. raise NotImplementedError()
  865. # return UiObject(self.session, self.jsonrpc.getParent(self.selector))
  866. def __getitem__(self, index):
  867. """
  868. Raises:
  869. IndexError
  870. """
  871. if isinstance(self.selector, six.string_types):
  872. raise IndexError(
  873. "Index is not supported when UiObject returned by child_by_xxx")
  874. selector = self.selector.clone()
  875. selector.update_instance(index)
  876. return UiObject(self.session, selector)
  877. @property
  878. def count(self):
  879. return self.jsonrpc.count(self.selector)
  880. def __len__(self):
  881. return self.count
  882. def __iter__(self):
  883. obj, length = self, self.count
  884. class Iter(object):
  885. def __init__(self):
  886. self.index = -1
  887. def next(self):
  888. self.index += 1
  889. if self.index < length:
  890. return obj[self.index]
  891. else:
  892. raise StopIteration()
  893. __next__ = next
  894. return Iter()
  895. def right(self, **kwargs):
  896. def onrightof(rect1, rect2):
  897. left, top, right, bottom = intersect(rect1, rect2)
  898. return rect2["left"] - rect1["right"] if top < bottom else -1
  899. return self.__view_beside(onrightof, **kwargs)
  900. def left(self, **kwargs):
  901. def onleftof(rect1, rect2):
  902. left, top, right, bottom = intersect(rect1, rect2)
  903. return rect1["left"] - rect2["right"] if top < bottom else -1
  904. return self.__view_beside(onleftof, **kwargs)
  905. def up(self, **kwargs):
  906. def above(rect1, rect2):
  907. left, top, right, bottom = intersect(rect1, rect2)
  908. return rect1["top"] - rect2["bottom"] if left < right else -1
  909. return self.__view_beside(above, **kwargs)
  910. def down(self, **kwargs):
  911. def under(rect1, rect2):
  912. left, top, right, bottom = intersect(rect1, rect2)
  913. return rect2["top"] - rect1["bottom"] if left < right else -1
  914. return self.__view_beside(under, **kwargs)
  915. def __view_beside(self, onsideof, **kwargs):
  916. bounds = self.info["bounds"]
  917. min_dist, found = -1, None
  918. for ui in UiObject(self.session, Selector(**kwargs)):
  919. dist = onsideof(bounds, ui.info["bounds"])
  920. if dist >= 0 and (min_dist < 0 or dist < min_dist):
  921. min_dist, found = dist, ui
  922. return found
  923. @property
  924. def fling(self):
  925. """
  926. Args:
  927. dimention (str): one of "vert", "vertically", "vertical", "horiz", "horizental", "horizentally"
  928. action (str): one of "forward", "backward", "toBeginning", "toEnd", "to"
  929. """
  930. jsonrpc = self.jsonrpc
  931. selector = self.selector
  932. class _Fling(object):
  933. def __init__(self):
  934. self.vertical = True
  935. self.action = 'forward'
  936. def __getattr__(self, key):
  937. if key in ["horiz", "horizental", "horizentally"]:
  938. self.vertical = False
  939. return self
  940. if key in ['vert', 'vertically', 'vertical']:
  941. self.vertical = True
  942. return self
  943. if key in [
  944. "forward", "backward", "toBeginning", "toEnd", "to"
  945. ]:
  946. self.action = key
  947. return self
  948. raise ValueError("invalid prop %s" % key)
  949. def __call__(self, max_swipes=500, **kwargs):
  950. if self.action == "forward":
  951. return jsonrpc.flingForward(selector, self.vertical)
  952. elif self.action == "backward":
  953. return jsonrpc.flingBackward(selector, self.vertical)
  954. elif self.action == "toBeginning":
  955. return jsonrpc.flingToBeginning(selector, self.vertical,
  956. max_swipes)
  957. elif self.action == "toEnd":
  958. return jsonrpc.flingToEnd(selector, self.vertical,
  959. max_swipes)
  960. return _Fling()
  961. @property
  962. def scroll(self):
  963. """
  964. Args:
  965. dimention (str): one of "vert", "vertically", "vertical", "horiz", "horizental", "horizentally"
  966. action (str): one of "forward", "backward", "toBeginning", "toEnd", "to"
  967. """
  968. selector = self.selector
  969. jsonrpc = self.jsonrpc
  970. class _Scroll(object):
  971. def __init__(self):
  972. self.vertical = True
  973. self.action = 'forward'
  974. def __getattr__(self, key):
  975. if key in ["horiz", "horizental", "horizentally"]:
  976. self.vertical = False
  977. return self
  978. if key in ['vert', 'vertically', 'vertical']:
  979. self.vertical = True
  980. return self
  981. if key in [
  982. "forward", "backward", "toBeginning", "toEnd", "to"
  983. ]:
  984. self.action = key
  985. return self
  986. raise ValueError("invalid prop %s" % key)
  987. def __call__(self, steps=20, max_swipes=500, **kwargs):
  988. if self.action in ["forward", "backward"]:
  989. method = jsonrpc.scrollForward if self.action == "forward" else jsonrpc.scrollBackward
  990. return method(selector, self.vertical, steps)
  991. elif self.action == "toBeginning":
  992. return jsonrpc.scrollToBeginning(selector, self.vertical,
  993. max_swipes, steps)
  994. elif self.action == "toEnd":
  995. return jsonrpc.scrollToEnd(selector, self.vertical,
  996. max_swipes, steps)
  997. elif self.action == "to":
  998. return jsonrpc.scrollTo(selector, Selector(**kwargs),
  999. self.vertical)
  1000. return _Scroll()